From 6c6b6f38893b74755d365940f8187b01e7e02685 Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Wed, 10 Feb 2021 18:44:55 +0800 Subject: [PATCH 01/15] Add integration tests --- brainpy/core/runner.py | 5 +- brainpy/integration/diff_equation.py | 19 +- brainpy/integration/integrator.py | 167 +++++++-------- brainpy/integration/utils.py | 13 +- brainpy/tools/ast2code.py | 2 +- brainpy/tools/codes.py | 9 +- tests/{ => integration}/test_diff_equation.py | 132 +++++++----- tests/integration/test_integrators.py | 192 ++++++++++++++++++ tests/integration/test_utils.py | 97 +++++++++ 9 files changed, 464 insertions(+), 172 deletions(-) rename tests/{ => integration}/test_diff_equation.py (53%) create mode 100644 tests/integration/test_integrators.py create mode 100644 tests/integration/test_utils.py diff --git a/brainpy/core/runner.py b/brainpy/core/runner.py index ace5b053..51f61b42 100644 --- a/brainpy/core/runner.py +++ b/brainpy/core/runner.py @@ -636,10 +636,9 @@ class Runner(object): new_line, args, kwargs = tools.replace_func(line, int_func_name) # append code line of argument replacement func_args = v.diff_eq.func_args - append_lines = [indent + f'_{v.py_func_name}_{func_args[i]} = {args[i]}' - for i in range(len(args))] + append_lines = [indent + f'_{func_args[i]} = {args[i]}' for i in range(len(args))] for arg in func_args[len(args):]: - append_lines.append(indent + f'_{v.py_func_name}_{arg} = {kwargs[arg]}') + append_lines.append(indent + f'_{arg} = {kwargs[arg]}') # append numerical integration code lines append_lines.extend([indent + l for l in v.update_code.split('\n')]) diff --git a/brainpy/integration/diff_equation.py b/brainpy/integration/diff_equation.py index 2dd9fada..1ff322db 100644 --- a/brainpy/integration/diff_equation.py +++ b/brainpy/integration/diff_equation.py @@ -105,7 +105,7 @@ class DiffEquation(object): # analyse function code res = utils.analyse_diff_eq(self.code) self.expressions = [Expression(v, expr) for v, expr in zip(res.variables, res.expressions)] - self.returns = res.returns + self.return_intermediates = res.return_intermediates self.return_type = res.return_type self.f_expr = None self.g_expr = None @@ -199,7 +199,7 @@ class DiffEquation(object): return_expressions.append(Expression(f'_df{self.var_name}_dt', dif_eq_code)) # needed variables need_vars = tools.get_identifiers(dif_eq_code) - need_vars |= tools.get_identifiers(', '.join(self.returns)) + need_vars |= tools.get_identifiers(', '.join(self.return_intermediates)) # get the total return expressions for expr in self.expressions[::-1]: if expr.var_name in need_vars: @@ -212,6 +212,9 @@ class DiffEquation(object): return return_expressions[::-1] def get_g_expressions(self): + if self.g_expr is None: + return [] + if self.is_functional_noise: return_expressions = [] # the derivative expression @@ -291,19 +294,19 @@ class DiffEquation(object): A list of expressions. """ return self._replace_expressions(self.get_f_expressions(), - name=name, y_sub=y_sub, t_sub=t_sub) + name=name, + y_sub=y_sub, + t_sub=t_sub) def replace_g_expressions(self, name, y_sub, t_sub=None): if self.is_functional_noise: return self._replace_expressions(self.get_g_expressions(), - name=name, y_sub=y_sub, t_sub=t_sub) + name=name, + y_sub=y_sub, + t_sub=t_sub) else: return [] - @property - def is_multi_return(self): - return len(self.returns) > 0 - @property def is_stochastic(self): if self.g_expr is not None: diff --git a/brainpy/integration/integrator.py b/brainpy/integration/integrator.py index 39cafa94..2525e78f 100644 --- a/brainpy/integration/integrator.py +++ b/brainpy/integration/integrator.py @@ -3,14 +3,12 @@ import numpy as np import sympy -from .diff_equation import DiffEquation -from .utils import get_mapping_scope -from .utils import str2sympy -from .utils import sympy2str +from . import diff_equation +from . import utils from .. import backend +from .. import errors from .. import profile from .. import tools -from ..errors import IntegratorError __all__ = [ 'get_integrator', @@ -59,11 +57,11 @@ def get_integrator(method): class Integrator(object): def __init__(self, diff_eq): - if not isinstance(diff_eq, DiffEquation): + if not isinstance(diff_eq, diff_equation.DiffEquation): if diff_eq.__class__.__name__ != 'function': - raise IntegratorError('"diff_eq" must be a function or an instance of DiffEquation .') + raise errors.IntegratorError('"diff_eq" must be a function or an instance of DiffEquation .') else: - diff_eq = DiffEquation(func=diff_eq) + diff_eq = diff_equation.DiffEquation(func=diff_eq) self.diff_eq = diff_eq self._update_code = None self._update_func = None @@ -73,11 +71,11 @@ class Integrator(object): def _compile(self): # function arguments - func_args = ', '.join([f'_{self.py_func_name}_{arg}' for arg in self.diff_eq.func_args]) + func_args = ', '.join([f'_{arg}' for arg in self.diff_eq.func_args]) # function codes func_code = f'def {self.py_func_name}({func_args}): \n' - func_code += tools.indent(self._update_code + '\n' + f'return _{self.py_func_name}_res') + func_code += tools.indent(self._update_code + '\n' + f'return _res') tools.NoiseHandler.normal_pattern.sub( tools.NoiseHandler.vector_replace_f, func_code) @@ -87,7 +85,7 @@ class Integrator(object): if profile.is_jit() and callable(v_): v_ = tools.numba_func(v_) code_scopes[k_] = v_ - code_scopes.update(get_mapping_scope()) + code_scopes.update(utils.get_mapping_scope()) code_scopes['_normal_like_'] = backend.normal_like # function compilation @@ -189,7 +187,6 @@ class Euler(Integrator): def get_integral_step(diff_eq, *args): dt = profile.get_dt() var_name = diff_eq.var_name - func_name = diff_eq.func_name var = sympy.Symbol(var_name, real=True) # get code lines of df part @@ -208,16 +205,15 @@ class Euler(Integrator): # update expression update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {sympy2str(update)}') + code_lines.append(f'{var_name} = {utils.sympy2str(update)}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code @@ -268,7 +264,6 @@ class RK2(Integrator): dt = profile.get_dt() t_name = diff_eq.t_name var_name = diff_eq.var_name - func_name = diff_eq.func_name var = sympy.Symbol(var_name, real=True) # get code lines of k1 df part @@ -277,8 +272,8 @@ class RK2(Integrator): code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') # k1 -> k2 increment - y_1_to_2 = f'_{func_name}_{var_name}_k1_to_k2' - t_1_to_2 = f'_{func_name}_t_k1_to_k2' + y_1_to_2 = f'_{var_name}_k1_to_k2' + t_1_to_2 = f'_t_k1_to_k2' code_lines.append(f'{y_1_to_2} = {var_name} + {beta * dt} * _df{var_name}_dt_k1') code_lines.append(f'{t_1_to_2} = {t_name} + {beta * dt}') @@ -306,16 +301,15 @@ class RK2(Integrator): # update expression update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {sympy2str(update)}') + code_lines.append(f'{var_name} = {utils.sympy2str(update)}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code @@ -376,7 +370,6 @@ class Heun(Integrator): if diff_eq.is_functional_noise: dt = profile.get_dt() var_name = diff_eq.var_name - func_name = diff_eq.func_name var = sympy.Symbol(var_name, real=True) # k1 part # @@ -396,7 +389,7 @@ class Heun(Integrator): code_lines.append(f'_dg{var_name}_dt_k1 = {g_k1_expressions[-1].code}') # k1 - code_lines.append(f'_{func_name}_k1 = {var_name} + _df{var_name}_dt_k1 * {dt} + ' + code_lines.append(f'_k1 = {var_name} + _df{var_name}_dt_k1 * {dt} + ' f'_dg{var_name}_dt_k1 * {dW_sb.name}') # k2 part # @@ -404,7 +397,7 @@ class Heun(Integrator): # df dfdt = sympy.Symbol(f'_df{var_name}_dt') - f_k2_expressions = diff_eq.replace_f_expressions('k2', y_sub=f'_{func_name}_k1') + f_k2_expressions = diff_eq.replace_f_expressions('k2', y_sub='_k1') if len(f_k2_expressions): code_lines.extend([str(expr) for expr in f_k2_expressions[:-1]]) code_lines.append(f'_df{var_name}_dt_k2 = {f_k2_expressions[-1].code}') @@ -414,7 +407,7 @@ class Heun(Integrator): # dg dgdt = sympy.Symbol(f'_dg{var_name}_dt') - g_k2_expressions = diff_eq.replace_f_expressions('k2', y_sub=f'_{func_name}_k1') + g_k2_expressions = diff_eq.replace_f_expressions('k2', y_sub='_k1') if len(g_k2_expressions): code_lines.extend([str(expr) for expr in g_k2_expressions[:-1]]) code_lines.append(f'_dg{var_name}_dt_k2 = {g_k2_expressions[-1].code}') @@ -424,16 +417,15 @@ class Heun(Integrator): # update expression update = var + dfdt * dt + dgdt * dW_sb - code_lines.append(f'{var_name} = {sympy2str(update)}') + code_lines.append(f'{var_name} = {utils.sympy2str(update)}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code else: @@ -512,7 +504,6 @@ class RK3(Integrator): dt = profile.get_dt() t_name = diff_eq.t_name var_name = diff_eq.var_name - func_name = diff_eq.func_name var = sympy.Symbol(var_name, real=True) # get code lines of k1 df part @@ -521,8 +512,8 @@ class RK3(Integrator): code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') # k1 -> k2 increment - y_1_to_2 = f'_{func_name}_{var_name}_k1_to_k2' - t_1_to_2 = f'_{func_name}_t_k1_to_k2' + y_1_to_2 = f'_{var_name}_k1_to_k2' + t_1_to_2 = f'_t_k1_to_k2' code_lines.append(f'{y_1_to_2} = {var_name} + {dt / 2} * _df{var_name}_dt_k1') code_lines.append(f'{t_1_to_2} = {t_name} + {dt / 2}') @@ -535,8 +526,8 @@ class RK3(Integrator): code_lines.append(f'_df{var_name}_dt_k2 = {k2_expressions[-1].code}') # get code lines of k3 df part - y_1_to_3 = f'_{func_name}_{var_name}_k1_to_k3' - t_1_to_3 = f'_{func_name}_t_k1_to_k3' + y_1_to_3 = f'_{var_name}_k1_to_k3' + t_1_to_3 = f'_t_k1_to_k3' code_lines.append(f'{y_1_to_3} = {var_name} - {dt} * _df{var_name}_dt_k1 + {2 * dt} * _df{var_name}_dt_k2') code_lines.append(f'{t_1_to_3} = {t_name} + {dt}') k3_expressions = diff_eq.replace_f_expressions('k3', y_sub=y_1_to_3, t_sub=t_1_to_3) @@ -558,16 +549,15 @@ class RK3(Integrator): # update expression update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {sympy2str(update)}') + code_lines.append(f'{var_name} = {utils.sympy2str(update)}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code @@ -611,7 +601,6 @@ class RK4(Integrator): dt = profile.get_dt() t_name = diff_eq.t_name var_name = diff_eq.var_name - func_name = diff_eq.func_name var = sympy.Symbol(var_name, real=True) # get code lines of k1 df part @@ -620,8 +609,8 @@ class RK4(Integrator): code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') # k1 -> k2 increment - y_1_to_2 = f'_{func_name}_{var_name}_k1_to_k2' - t_1_to_2 = f'_{func_name}_t_k1_to_k2' + y_1_to_2 = f'_{var_name}_k1_to_k2' + t_1_to_2 = f'_t_k1_to_k2' code_lines.append(f'{y_1_to_2} = {var_name} + {dt / 2} * _df{var_name}_dt_k1') code_lines.append(f'{t_1_to_2} = {t_name} + {dt / 2}') @@ -634,8 +623,8 @@ class RK4(Integrator): code_lines.append(f'_df{var_name}_dt_k2 = {k2_expressions[-1].code}') # get code lines of k3 df part - y_2_to_3 = f'_{func_name}_{var_name}_k2_to_k3' - t_2_to_3 = f'_{func_name}_t_k2_to_k3' + y_2_to_3 = f'_{var_name}_k2_to_k3' + t_2_to_3 = f'_t_k2_to_k3' code_lines.append(f'{y_2_to_3} = {var_name} + {dt / 2} * _df{var_name}_dt_k2') code_lines.append(f'{t_2_to_3} = {t_name} + {dt / 2}') k3_expressions = diff_eq.replace_f_expressions('k3', y_sub=y_2_to_3, t_sub=t_2_to_3) @@ -643,8 +632,8 @@ class RK4(Integrator): code_lines.append(f'_df{var_name}_dt_k3 = {k3_expressions[-1].code}') # get code lines of k4 df part - y_3_to_4 = f'_{func_name}_{var_name}_k3_to_k4' - t_3_to_4 = f'_{func_name}_t_k3_to_k4' + y_3_to_4 = f'_{var_name}_k3_to_k4' + t_3_to_4 = f'_t_k3_to_k4' code_lines.append(f'{y_3_to_4} = {var_name} + {dt} * _df{var_name}_dt_k3') code_lines.append(f'{t_3_to_4} = {t_name} + {dt}') k4_expressions = diff_eq.replace_f_expressions('k4', y_sub=y_3_to_4, t_sub=t_3_to_4) @@ -666,16 +655,15 @@ class RK4(Integrator): # update expression update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {sympy2str(update)}') + code_lines.append(f'{var_name} = {utils.sympy2str(update)}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code @@ -721,7 +709,6 @@ class RK4Alternative(Integrator): dt = profile.get_dt() t_name = diff_eq.t_name var_name = diff_eq.var_name - func_name = diff_eq.func_name var = sympy.Symbol(var_name, real=True) # get code lines of k1 df part @@ -730,8 +717,8 @@ class RK4Alternative(Integrator): code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') # k1 -> k2 increment - y_1_to_2 = f'_{func_name}_{var_name}_k1_to_k2' - t_1_to_2 = f'_{func_name}_t_k1_to_k2' + y_1_to_2 = f'_{var_name}_k1_to_k2' + t_1_to_2 = f'_t_k1_to_k2' code_lines.append(f'{y_1_to_2} = {var_name} + {dt / 3} * _df{var_name}_dt_k1') code_lines.append(f'{t_1_to_2} = {t_name} + {dt / 3}') @@ -744,8 +731,8 @@ class RK4Alternative(Integrator): code_lines.append(f'_df{var_name}_dt_k2 = {k2_expressions[-1].code}') # get code lines of k3 df part - y_1_to_3 = f'_{func_name}_{var_name}_k1_to_k3' - t_1_to_3 = f'_{func_name}_t_k1_to_k3' + y_1_to_3 = f'_{var_name}_k1_to_k3' + t_1_to_3 = f'__t_k1_to_k3' code_lines.append(f'{y_1_to_3} = {var_name} - {dt / 3} * _df{var_name}_dt_k1 + {dt} * _df{var_name}_dt_k2') code_lines.append(f'{t_1_to_3} = {t_name} + {dt * 2 / 3}') k3_expressions = diff_eq.replace_f_expressions('k3', y_sub=y_1_to_3, t_sub=t_1_to_3) @@ -753,8 +740,8 @@ class RK4Alternative(Integrator): code_lines.append(f'_df{var_name}_dt_k3 = {k3_expressions[-1].code}') # get code lines of k4 df part - y_1_to_4 = f'_{func_name}_{var_name}_k1_to_k4' - t_1_to_4 = f'_{func_name}_t_k1_to_k4' + y_1_to_4 = f'_{var_name}_k1_to_k4' + t_1_to_4 = f'_t_k1_to_k4' code_lines.append(f'{y_1_to_4} = {var_name} + {dt} * _df{var_name}_dt_k1 - {dt} * _df{var_name}_dt_k2' f'+ {dt} * _df{var_name}_dt_k3') code_lines.append(f'{t_1_to_4} = {t_name} + {dt}') @@ -777,16 +764,15 @@ class RK4Alternative(Integrator): # update expression update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {sympy2str(update)}') + code_lines.append(f'{var_name} = {utils.sympy2str(update)}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code @@ -861,9 +847,9 @@ class ExponentialEuler(Integrator): # get the linear system using sympy f_res = f_expressions[-1] - df_expr = str2sympy(f_res.code).expr.expand() + df_expr = utils.str2sympy(f_res.code).expr.expand() s_df = sympy.Symbol(f"{f_res.var_name}") - code_lines.append(f'{s_df.name} = {sympy2str(df_expr)}') + code_lines.append(f'{s_df.name} = {utils.sympy2str(df_expr)}') var = sympy.Symbol(diff_eq.var_name, real=True) # get df part @@ -873,19 +859,19 @@ class ExponentialEuler(Integrator): if df_expr.has(var): # linear linear = sympy.collect(df_expr, var, evaluate=False)[var] - code_lines.append(f'{s_linear.name} = {sympy2str(linear)}') + code_lines.append(f'{s_linear.name} = {utils.sympy2str(linear)}') # linear exponential linear_exp = sympy.exp(linear * dt) - code_lines.append(f'{s_linear_exp.name} = {sympy2str(linear_exp)}') + code_lines.append(f'{s_linear_exp.name} = {utils.sympy2str(linear_exp)}') # df part df_part = (s_linear_exp - 1) / s_linear * s_df - code_lines.append(f'{s_df_part.name} = {sympy2str(df_part)}') + code_lines.append(f'{s_df_part.name} = {utils.sympy2str(df_part)}') else: # linear exponential code_lines.append(f'{s_linear_exp.name} = sqrt({dt})') # df part - code_lines.append(f'{s_df_part.name} = {sympy2str(dt * s_df)}') + code_lines.append(f'{s_df_part.name} = {utils.sympy2str(dt * s_df)}') # get dg part if diff_eq.is_stochastic: @@ -906,14 +892,13 @@ class ExponentialEuler(Integrator): update = var + s_df_part + s_dg_part * s_linear_exp # The actual update step - code_lines.append(f'{diff_eq.var_name} = {sympy2str(update)}') - return_expr = ', '.join([diff_eq.var_name] + diff_eq.returns) - code_lines.append(f'_{diff_eq.func_name}_res = {return_expr}') + code_lines.append(f'{diff_eq.var_name} = {utils.sympy2str(update)}') + return_expr = ', '.join([diff_eq.var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code @@ -966,7 +951,6 @@ class MilsteinIto(Integrator): dt = profile.get_dt() var_name = diff_eq.var_name - func_name = diff_eq.func_name # k1 part # # ------- # @@ -984,10 +968,10 @@ class MilsteinIto(Integrator): # high order part # # --------------- # - k1_expr = f'_{func_name}_k1 = {var_name} + _df{var_name}_dt * {dt} + ' \ + k1_expr = f'_k1 = {var_name} + _df{var_name}_dt * {dt} + ' \ f'_dg{var_name}_dt * sqrt({dt})' high_order = sympy.Symbol(f'_dg{var_name}_high_order') - g_k2_expressions = diff_eq.replace_g_expressions('k2', y_sub=f'_{func_name}_k1') + g_k2_expressions = diff_eq.replace_g_expressions('k2', y_sub=f'_k1') # dg high order if len(g_k2_expressions): @@ -1004,13 +988,12 @@ class MilsteinIto(Integrator): f'_dg{var_name}_dt * {dW_sb.name}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code @@ -1078,7 +1061,6 @@ class MilsteinStra(Integrator): dt = profile.get_dt() var_name = diff_eq.var_name - func_name = diff_eq.func_name # k1 part # # ------- # @@ -1097,10 +1079,10 @@ class MilsteinStra(Integrator): # high order part # # --------------- # - k1_expr = f'_{func_name}_k1 = {var_name} + _df{var_name}_dt * {dt} + ' \ + k1_expr = f'_k1 = {var_name} + _df{var_name}_dt * {dt} + ' \ f'_dg{var_name}_dt * sqrt({dt})' high_order = sympy.Symbol(f'_dg{var_name}_high_order') - g_k2_expressions = diff_eq.replace_g_expressions('k2', y_sub=f'_{func_name}_k1') + g_k2_expressions = diff_eq.replace_g_expressions('k2', y_sub=f'_k1') if len(g_k2_expressions): code_lines.append(k1_expr) code_lines.extend([str(expr) for expr in g_k2_expressions[:-1]]) @@ -1115,13 +1097,12 @@ class MilsteinStra(Integrator): f'_dg{var_name}_dt * {dW_sb.name}') # multiple returns - return_expr = ', '.join([var_name] + diff_eq.returns) - code_lines.append(f'_{func_name}_res = {return_expr}') + return_expr = ', '.join([var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') # final code = '\n'.join(code_lines) - subs_dict = {arg: f'_{diff_eq.func_name}_{arg}' for arg in - diff_eq.func_args + diff_eq.expr_names} + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} code = tools.word_replace(code, subs_dict) return code diff --git a/brainpy/integration/utils.py b/brainpy/integration/utils.py index 3b1ba216..796f9455 100644 --- a/brainpy/integration/utils.py +++ b/brainpy/integration/utils.py @@ -426,11 +426,6 @@ def sympy2str(sympy_expr): class DiffEquationAnalyser(ast.NodeTransformer): - expression_ops = { - 'Add': '+', 'Sub': '-', 'Mult': '*', 'Div': '/', - 'Mod': '%', 'Pow': '**', 'BitXor': '^', 'BitAnd': '&', - } - def __init__(self): self.variables = [] self.expressions = [] @@ -511,7 +506,7 @@ class DiffEquationAnalyser(ast.NodeTransformer): self.g_expr = ("_g_res_", tools.ast2code(ast.fix_missing_locations(value.elts[1]))) else: raise errors.DiffEquationError('Cannot parse return expression. It should have the ' - 'format of "(f, [g]), [return values]"') + 'format of "(f, [g]), [*return_values]"') else: self.return_type = 'x' if isinstance(value, ast.Name): # a name return @@ -539,10 +534,10 @@ class DiffEquationAnalyser(ast.NodeTransformer): raise errors.DiffEquationError('Do not support "with" block in differential equation.') def visit_Raise(self, node): - raise errors.DiffEquationError('Do not support "raise" statement.') + raise errors.DiffEquationError('Do not support "raise" statement in differential equation.') def visit_Delete(self, node): - raise errors.DiffEquationError('Do not support "del" operation.') + raise errors.DiffEquationError('Do not support "del" operation in differential equation.') def analyse_diff_eq(eq_code): @@ -553,7 +548,7 @@ def analyse_diff_eq(eq_code): res = tools.DictPlus(variables=analyser.variables, expressions=analyser.expressions, - returns=analyser.returns, + return_intermediates=analyser.returns, return_type=analyser.return_type, f_expr=analyser.f_expr, g_expr=analyser.g_expr) diff --git a/brainpy/tools/ast2code.py b/brainpy/tools/ast2code.py index 6510fdaf..79c204cb 100644 --- a/brainpy/tools/ast2code.py +++ b/brainpy/tools/ast2code.py @@ -314,7 +314,7 @@ class Transformer(ast.NodeVisitor): self.write(')') if getattr(node, 'returns', None): self.write(' -> ') - self.visit(node.returns) + self.visit(node.return_intermediates) self.write(':') self.write_newline() diff --git a/brainpy/tools/codes.py b/brainpy/tools/codes.py index 25a897d6..7ee79708 100644 --- a/brainpy/tools/codes.py +++ b/brainpy/tools/codes.py @@ -102,10 +102,6 @@ def get_identifiers(expr, include_numbers=False): return (identifiers - _ID_KEYWORDS) | numbers - - - - class NoiseHandler(object): normal_pattern = re.compile(r'(_normal_like_)\((\w+)\)') @@ -163,7 +159,7 @@ class FuncCallFinder(ast.NodeTransformer): else: s = ast2code(ast.fix_missing_locations(kv.value)) self.kwargs[kv.arg] = s.strip() - return ast.Name(f'_{self.name}_res') + return ast.Name('_res') else: args = [self.visit(arg) for arg in node.args] keywords = [self.visit(kv) for kv in node.keywords] @@ -609,7 +605,8 @@ def word_replace(expr, substitutions): banana*_b+c5+8+func(A) """ for var, replace_var in substitutions.items(): - expr = re.sub(r'\b' + var + r'\b', str(replace_var), expr) + # expr = re.sub(r'\b' + var + r'\b', str(replace_var), expr) + expr = re.sub(r'\b(? Date: Wed, 17 Feb 2021 00:47:29 +0800 Subject: [PATCH 02/15] add Garbage Collect for bifurcation analysis --- brainpy/analysis/bifurcation.py | 137 +++++++++++++++----------------- brainpy/analysis/solver.py | 9 +-- brainpy/analysis/utils.py | 75 ++++++++++------- brainpy/core/neurons.py | 13 ++- brainpy/core/synapses.py | 13 ++- 5 files changed, 128 insertions(+), 119 deletions(-) diff --git a/brainpy/analysis/bifurcation.py b/brainpy/analysis/bifurcation.py index 5794ab18..00f77cef 100644 --- a/brainpy/analysis/bifurcation.py +++ b/brainpy/analysis/bifurcation.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import gc from collections import OrderedDict import matplotlib.pyplot as plt @@ -228,9 +229,6 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): options=options) self.fixed_points = None - self.limit_cycle_mon = None - self.limit_cycle_p0 = None - self.limit_cycle_p1 = None def plot_bifurcation(self, show=False): print('plot bifurcation ...') @@ -333,72 +331,65 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): if var not in [self.x_var, self.y_var]: raise errors.AnalyzerError() - if self.limit_cycle_mon is None: - all_xs, all_ys, all_p0, all_p1 = [], [], [], [] - - # unstable node - unstable_node = self.fixed_points[utils._2D_UNSTABLE_NODE] - all_xs.extend(unstable_node[self.x_var]) - all_ys.extend(unstable_node[self.y_var]) - if len(self.dpar_names) == 1: - all_p0.extend(unstable_node['p']) - elif len(self.dpar_names) == 2: - all_p0.extend(unstable_node['p0']) - all_p1.extend(unstable_node['p1']) - else: - raise ValueError - - # unstable focus - unstable_focus = self.fixed_points[utils._2D_UNSTABLE_FOCUS] - all_xs.extend(unstable_focus[self.x_var]) - all_ys.extend(unstable_focus[self.y_var]) - if len(self.dpar_names) == 1: - all_p0.extend(unstable_focus['p']) - elif len(self.dpar_names) == 2: - all_p0.extend(unstable_focus['p0']) - all_p1.extend(unstable_focus['p1']) - else: - raise ValueError - - # format points - all_xs = np.array(all_xs) - all_ys = np.array(all_ys) - all_p0 = np.array(all_p0) - all_p1 = np.array(all_p1) + all_xs, all_ys, all_p0, all_p1 = [], [], [], [] + + # unstable node + unstable_node = self.fixed_points[utils._2D_UNSTABLE_NODE] + all_xs.extend(unstable_node[self.x_var]) + all_ys.extend(unstable_node[self.y_var]) + if len(self.dpar_names) == 1: + all_p0.extend(unstable_node['p']) + elif len(self.dpar_names) == 2: + all_p0.extend(unstable_node['p0']) + all_p1.extend(unstable_node['p1']) + else: + raise ValueError - # fixed variables - fixed_vars = dict() - for key, val in self.fixed_vars.items(): - fixed_vars[key] = val - fixed_vars[self.dpar_names[0]] = all_p0 - if len(self.dpar_names) == 2: - fixed_vars[self.dpar_names[1]] = all_p1 - - # initialize neuron group - length = all_xs.shape[0] - group = core.NeuGroup(self.model, - geometry=length, - monitors=self.dvar_names, - pars_update=self.pars_update) - - # group initial state - group.ST[self.x_var] = all_xs - group.ST[self.y_var] = all_ys - for key, val in fixed_vars.items(): - if key in group.ST: - group.ST[key] = val - - # run neuron group - group.runner = core.TrajectoryRunner(group, - target_vars=self.dvar_names, - fixed_vars=fixed_vars) - group.run(duration=duration, inputs=inputs) - - self.limit_cycle_mon = group.mon - self.limit_cycle_p0 = all_p0 - self.limit_cycle_p1 = all_p1 + # unstable focus + unstable_focus = self.fixed_points[utils._2D_UNSTABLE_FOCUS] + all_xs.extend(unstable_focus[self.x_var]) + all_ys.extend(unstable_focus[self.y_var]) + if len(self.dpar_names) == 1: + all_p0.extend(unstable_focus['p']) + elif len(self.dpar_names) == 2: + all_p0.extend(unstable_focus['p0']) + all_p1.extend(unstable_focus['p1']) else: - length = self.limit_cycle_mon[var].shape[1] + raise ValueError + + # format points + all_xs = np.array(all_xs) + all_ys = np.array(all_ys) + all_p0 = np.array(all_p0) + all_p1 = np.array(all_p1) + + # fixed variables + fixed_vars = dict() + for key, val in self.fixed_vars.items(): + fixed_vars[key] = val + fixed_vars[self.dpar_names[0]] = all_p0 + if len(self.dpar_names) == 2: + fixed_vars[self.dpar_names[1]] = all_p1 + + # initialize neuron group + length = all_xs.shape[0] + group = core.NeuGroup(self.model, + geometry=length, + monitors=self.dvar_names, + pars_update=self.pars_update) + + # group initial state + group.ST[self.x_var] = all_xs + group.ST[self.y_var] = all_ys + for key, val in fixed_vars.items(): + if key in group.ST: + group.ST[key] = val + + # run neuron group + group.runner = core.TrajectoryRunner(group, + target_vars=self.dvar_names, + fixed_vars=fixed_vars) + group.run(duration=duration, inputs=inputs) # find limit cycles limit_cycle_max = [] @@ -407,16 +398,16 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): p0_limit_cycle = [] p1_limit_cycle = [] for i in range(length): - data = self.limit_cycle_mon[var][:, i] + data = group.mon[var][:, i] max_index = utils.find_indexes_of_limit_cycle_max(data, tol=tol) if max_index[0] != -1: x_cycle = data[max_index[0]: max_index[1]] limit_cycle_max.append(data[max_index[1]]) limit_cycle_min.append(x_cycle.min()) # limit_cycle.append(x_cycle) - p0_limit_cycle.append(self.limit_cycle_p0[i]) + p0_limit_cycle.append(all_p0[i]) if len(self.dpar_names) == 2: - p1_limit_cycle.append(self.limit_cycle_p1[i]) + p1_limit_cycle.append(all_p1[i]) self.fixed_points['limit_cycle'] = {var: {'max': limit_cycle_max, 'min': limit_cycle_min, # 'cycle': limit_cycle @@ -443,7 +434,11 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): if show: plt.show() - + del group.ST + del group.mon + del group + gc.collect() + class FastSlowBifurcation(object): """Fast slow analysis analysis proposed by John Rinzel [1]_ [2]_ [3]_. diff --git a/brainpy/analysis/solver.py b/brainpy/analysis/solver.py index a67964b3..4ce67594 100644 --- a/brainpy/analysis/solver.py +++ b/brainpy/analysis/solver.py @@ -19,8 +19,7 @@ results = namedtuple('results', ['root', 'function_calls', 'iterations', 'conver @nb.njit -def brentq(f, a, b, args=(), xtol=2e-12, maxiter=100, - rtol=4 * np.finfo(float).eps): +def brentq(f, a, b, args=(), xtol=2e-14, maxiter=200, rtol=4 * np.finfo(float).eps): """ Find a root of a function in a bracketing interval using Brent's method adapted from Scipy's brentq. @@ -147,9 +146,9 @@ def brentq(f, a, b, args=(), xtol=2e-12, maxiter=100, if status == _ECONVERR: raise RuntimeError("Failed to converge") - x, funcalls, iterations = root, funcalls, itr + # x, funcalls, iterations = root, funcalls, itr - return x + return root, funcalls, itr @nb.njit @@ -190,7 +189,7 @@ def find_root_of_1d(f, f_points, args=(), tol=1e-8): f_i += 2 else: if not np.isnan(fr_sign) and fl_sign != fr_sign: - root = brentq(f, f_points[f_i - 1], f_points[f_i], args) + root, funcalls, itr = brentq(f, f_points[f_i - 1], f_points[f_i], args) if abs(f(root, *args)) < tol: roots.append(root) fl_sign = fr_sign diff --git a/brainpy/analysis/utils.py b/brainpy/analysis/utils.py index 26c612ff..8735c16b 100644 --- a/brainpy/analysis/utils.py +++ b/brainpy/analysis/utils.py @@ -20,19 +20,21 @@ __all__ = [ 'contain_unknown_symbol', ] -_SADDLE_NODE = 'saddle-node' -_1D_STABLE_POINT = 'stable-point' -_1D_UNSTABLE_POINT = 'unstable-point' +_CENTER_MANIFOLD = 'center manifold' +_SADDLE_NODE = 'saddle node' +_1D_STABLE_POINT = 'stable point' +_1D_UNSTABLE_POINT = 'unstable point' _2D_CENTER = 'center' -_2D_STABLE_NODE = 'stable-node' -_2D_STABLE_FOCUS = 'stable-focus' -_2D_STABLE_STAR = 'stable-star' -_2D_STABLE_LINE = 'stable-line' -_2D_UNSTABLE_NODE = 'unstable-node' -_2D_UNSTABLE_FOCUS = 'unstable-focus' -_2D_UNSTABLE_STAR = 'star' -_2D_UNSTABLE_LINE = 'unstable-line' -_2D_UNIFORM_MOTION = 'uniform-motion' +_2D_STABLE_NODE = 'stable node' +_2D_STABLE_FOCUS = 'stable focus' +_2D_STABLE_STAR = 'stable star' +_2D_STABLE_DEGENERATE = 'stable degenerate' +# _2D_STABLE_LINE = 'stable line' +_2D_UNSTABLE_NODE = 'unstable node' +_2D_UNSTABLE_FOCUS = 'unstable focus' +_2D_UNSTABLE_STAR = 'unstable star' +_2D_UNSTABLE_DEGENERATE = 'unstable degenerate' +_2D_UNSTABLE_LINE = 'unstable line' plot_scheme = { _1D_STABLE_POINT: {"color": 'tab:red'}, @@ -45,13 +47,17 @@ plot_scheme = { _2D_UNSTABLE_FOCUS: {"color": 'tab:cyan'}, _SADDLE_NODE: {"color": 'tab:blue'}, + _2D_CENTER: {'color': 'lime'}, + # _2D_UNIFORM_MOTION: {'color': 'red'}, - _2D_STABLE_LINE: {'color': 'orangered'}, + _CENTER_MANIFOLD: {'color': 'orangered'}, _2D_UNSTABLE_LINE: {'color': 'dodgerblue'}, - _2D_CENTER: {'color': 'lime'}, + _2D_UNSTABLE_STAR: {'color': 'green'}, _2D_STABLE_STAR: {'color': 'orange'}, - _2D_UNIFORM_MOTION: {'color': 'red'}, + + _2D_UNSTABLE_DEGENERATE: {'color': 'springgreen'}, + _2D_STABLE_DEGENERATE: {'color': 'blueviolet'}, } @@ -61,9 +67,9 @@ def get_1d_classification(): def get_2d_classification(): return [_SADDLE_NODE, _2D_CENTER, _2D_STABLE_NODE, _2D_STABLE_FOCUS, - _2D_STABLE_STAR, _2D_STABLE_LINE, _2D_UNSTABLE_NODE, + _2D_STABLE_STAR, _CENTER_MANIFOLD, _2D_UNSTABLE_NODE, _2D_UNSTABLE_FOCUS, _2D_UNSTABLE_STAR, _2D_UNSTABLE_LINE, - _2D_UNIFORM_MOTION] + _2D_STABLE_DEGENERATE, _2D_UNSTABLE_DEGENERATE] def stability_analysis(derivative): @@ -108,12 +114,10 @@ def stability_analysis(derivative): if q < 0: return _SADDLE_NODE elif q == 0: - if p < 0: - return _2D_STABLE_LINE - elif p > 0: - return _2D_UNSTABLE_LINE + if p <= 0: + return _CENTER_MANIFOLD else: - return _2D_UNIFORM_MOTION + return _2D_UNSTABLE_LINE else: # parabola e = p * p - 4 * q @@ -122,19 +126,32 @@ def stability_analysis(derivative): elif p > 0: if e < 0: return _2D_UNSTABLE_FOCUS - elif e == 0: - return _2D_UNSTABLE_STAR - else: + elif e > 0: return _2D_UNSTABLE_NODE + else: + w = np.linalg.eigvals(derivative) + if w[0] == w[1]: + return _2D_UNSTABLE_DEGENERATE + else: + return _2D_UNSTABLE_STAR else: if e < 0: return _2D_STABLE_FOCUS - elif e == 0: - return _2D_STABLE_STAR - else: + elif e > 0: return _2D_STABLE_NODE + else: + w = np.linalg.eigvals(derivative) + if w[0] == w[1]: + return _2D_STABLE_DEGENERATE + else: + return _2D_STABLE_STAR + + elif np.size(derivative) == 9: + pass + else: - raise ValueError('Unknown derivatives.') + raise ValueError('Unknown derivatives, only supports the jacobian ' + 'matrixwith the shape of(1), (2, 2), or (3, 3).') def rescale(min_max, scale=0.01): diff --git a/brainpy/core/neurons.py b/brainpy/core/neurons.py index 7dc79ec1..ef959c15 100644 --- a/brainpy/core/neurons.py +++ b/brainpy/core/neurons.py @@ -26,13 +26,12 @@ class NeuType(base.ObjType): if mode not in [constants.SCALAR_MODE, constants.VECTOR_MODE]: raise errors.ModelDefError('NeuType only support "scalar" or "vector".') - super(NeuType, self).__init__( - ST=ST, - requires=requires, - steps=steps, - name=name, - mode=mode, - hand_overs=hand_overs) + super(NeuType, self).__init__(ST=ST, + requires=requires, + steps=steps, + name=name, + mode=mode, + hand_overs=hand_overs) class NeuGroup(base.Ensemble): diff --git a/brainpy/core/synapses.py b/brainpy/core/synapses.py index 0548ffc3..1605becf 100644 --- a/brainpy/core/synapses.py +++ b/brainpy/core/synapses.py @@ -31,13 +31,12 @@ class SynType(base.ObjType): if mode not in [constants.SCALAR_MODE, constants.VECTOR_MODE, constants.MATRIX_MODE]: raise errors.ModelDefError('SynType only support "scalar", "vector" or "matrix".') - super(SynType, self).__init__( - ST=ST, - requires=requires, - steps=steps, - name=name, - mode=mode, - hand_overs=hand_overs) + super(SynType, self).__init__(ST=ST, + requires=requires, + steps=steps, + name=name, + mode=mode, + hand_overs=hand_overs) # inspect delay keys # ------------------ -- 2.34.1 From 15f5d85b136dc8b02a6edda8cd0b49d9e357c00d Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Thu, 18 Mar 2021 16:40:59 +0800 Subject: [PATCH 03/15] New design release with v1.0.0-rc1 --- .gitignore | 1 - brainpy/__init__.py | 43 +- brainpy/analysis/__init__.py | 1 - brainpy/analysis/base.py | 124 +- brainpy/analysis/bifurcation.py | 116 +- brainpy/analysis/phase_plane.py | 42 +- brainpy/analysis/solver.py | 20 +- brainpy/analysis/stability.py | 116 ++ brainpy/analysis/utils.py | 180 +-- brainpy/backend/__init__.py | 133 +- brainpy/backend/operators/__init__.py | 1 + brainpy/backend/operators/bk_jax.py | 34 + brainpy/backend/operators/bk_numba_cpu.py | 42 + brainpy/backend/operators/bk_numba_cuda.py | 4 + .../bk_numba_overload.py} | 24 +- brainpy/backend/operators/bk_numpy.py | 40 + brainpy/backend/operators/bk_pytorch.py | 29 + brainpy/backend/operators/bk_tensorflow.py | 35 + brainpy/backend/operators/standard.py | 66 + brainpy/backend/runners/__init__.py | 1 + brainpy/backend/runners/general_runner.py | 256 ++++ brainpy/backend/runners/jax_runner.py | 5 + brainpy/backend/runners/numba_cpu_runner.py | 406 ++++++ brainpy/backend/runners/numba_cuda_runner.py | 13 + brainpy/backend/runners/utils.py | 57 + brainpy/backend/utils.py | 44 - brainpy/connectivity/base.py | 164 +-- brainpy/connectivity/methods.py | 647 ++++----- brainpy/core/__init__.py | 8 - brainpy/core/base.py | 609 -------- brainpy/core/constants.py | 28 - brainpy/core/network.py | 331 ----- brainpy/core/neurons.py | 180 --- brainpy/core/runner.py | 1255 ----------------- brainpy/core/synapses.py | 243 ---- brainpy/core/types.py | 442 ------ brainpy/core/utils.py | 151 -- brainpy/errors.py | 11 +- brainpy/inputs.py | 266 +--- brainpy/integration/__init__.py | 74 - brainpy/integration/constants.py | 24 - brainpy/integration/diff_equation.py | 335 ----- brainpy/integration/integrator.py | 1109 --------------- brainpy/integrators/__init__.py | 9 + brainpy/integrators/ast_analysis.py | 193 +++ brainpy/integrators/constants.py | 95 ++ brainpy/integrators/dde/__init__.py | 1 + brainpy/integrators/delay_vars.py | 66 + brainpy/integrators/fde/__init__.py | 1 + brainpy/integrators/integrate_wrapper.py | 56 + brainpy/integrators/ode/__init__.py | 10 + brainpy/integrators/ode/exp_euler.py | 19 + .../integrators/ode/rk_adaptive_methods.py | 346 +++++ brainpy/integrators/ode/rk_methods.py | 380 +++++ brainpy/integrators/ode/wrapper.py | 274 ++++ brainpy/integrators/sde/__init__.py | 11 + brainpy/integrators/sde/common.py | 39 + brainpy/integrators/sde/euler_and_milstein.py | 273 ++++ brainpy/integrators/sde/exp_euler.py | 219 +++ brainpy/integrators/sde/srk_scalar.py | 437 ++++++ brainpy/integrators/sde/srk_strong.py | 443 ++++++ .../sympy_analysis.py} | 449 ++++-- brainpy/integrators/utils.py | 90 ++ brainpy/measure.py | 13 +- brainpy/profile.py | 311 +--- brainpy/simulation/__init__.py | 4 + brainpy/simulation/constants.py | 15 + brainpy/simulation/monitors.py | 53 + brainpy/simulation/network.py | 120 ++ brainpy/simulation/population.py | 285 ++++ brainpy/simulation/runner.py | 68 + brainpy/simulation/utils.py | 252 ++++ brainpy/tools/__init__.py | 1 - brainpy/tools/ast2code.py | 3 +- brainpy/tools/codes.py | 558 +------- brainpy/tools/dicts.py | 1 + brainpy/tools/functions.py | 125 -- brainpy/visualization/plots.py | 4 +- develop/benchmark/COBA/COBA.py | 14 +- develop/benchmark/COBA/COBA_brainpy.py | 18 +- develop/benchmark/COBAHH/COBAHH_brainpy.py | 10 +- develop/benchmark/CUBA/CUBA_brainpy.py | 18 +- develop/benchmark/scaling_test.py | 43 +- examples/FitzHugh_Nagumo.py | 49 + examples/hh_numba_cpu.py | 80 ++ requirements-dev.txt | 5 + requirements.txt | 3 - setup.py | 27 +- tests/backend/__init__.py | 1 + tests/backend/runners/__init__.py | 1 + tests/backend/runners/numba_runner.py | 90 ++ tests/integration/test_diff_equation.py | 2 +- tests/integration/test_integrators.py | 4 +- tests/integration/test_utils.py | 28 +- tests/integrators/__init__.py | 1 + tests/integrators/test_ast_analysis.py | 104 ++ tests/integrators/test_ode_adaptive_rk.py | 59 + tests/integrators/test_ode_rk.py | 53 + tests/integrators/test_sde_scalar.py | 73 + tests/test_dynamics.py | 4 +- tests/test_neugroup.py | 4 +- tests/test_ode_adaptive_rk.py | 59 + tests/test_ode_rk.py | 53 + tests/test_runner_gpu.py | 24 +- tests/test_trajectoty_runner.py | 8 +- 105 files changed, 6786 insertions(+), 6983 deletions(-) create mode 100644 brainpy/analysis/stability.py create mode 100644 brainpy/backend/operators/__init__.py create mode 100644 brainpy/backend/operators/bk_jax.py create mode 100644 brainpy/backend/operators/bk_numba_cpu.py create mode 100644 brainpy/backend/operators/bk_numba_cuda.py rename brainpy/backend/{numpy_overload.py => operators/bk_numba_overload.py} (87%) create mode 100644 brainpy/backend/operators/bk_numpy.py create mode 100644 brainpy/backend/operators/bk_pytorch.py create mode 100644 brainpy/backend/operators/bk_tensorflow.py create mode 100644 brainpy/backend/operators/standard.py create mode 100644 brainpy/backend/runners/__init__.py create mode 100644 brainpy/backend/runners/general_runner.py create mode 100644 brainpy/backend/runners/jax_runner.py create mode 100644 brainpy/backend/runners/numba_cpu_runner.py create mode 100644 brainpy/backend/runners/numba_cuda_runner.py create mode 100644 brainpy/backend/runners/utils.py delete mode 100644 brainpy/backend/utils.py delete mode 100644 brainpy/core/__init__.py delete mode 100644 brainpy/core/base.py delete mode 100644 brainpy/core/constants.py delete mode 100644 brainpy/core/network.py delete mode 100644 brainpy/core/neurons.py delete mode 100644 brainpy/core/runner.py delete mode 100644 brainpy/core/synapses.py delete mode 100644 brainpy/core/types.py delete mode 100644 brainpy/core/utils.py delete mode 100644 brainpy/integration/__init__.py delete mode 100644 brainpy/integration/constants.py delete mode 100644 brainpy/integration/diff_equation.py delete mode 100644 brainpy/integration/integrator.py create mode 100644 brainpy/integrators/__init__.py create mode 100644 brainpy/integrators/ast_analysis.py create mode 100644 brainpy/integrators/constants.py create mode 100644 brainpy/integrators/dde/__init__.py create mode 100644 brainpy/integrators/delay_vars.py create mode 100644 brainpy/integrators/fde/__init__.py create mode 100644 brainpy/integrators/integrate_wrapper.py create mode 100644 brainpy/integrators/ode/__init__.py create mode 100644 brainpy/integrators/ode/exp_euler.py create mode 100644 brainpy/integrators/ode/rk_adaptive_methods.py create mode 100644 brainpy/integrators/ode/rk_methods.py create mode 100644 brainpy/integrators/ode/wrapper.py create mode 100644 brainpy/integrators/sde/__init__.py create mode 100644 brainpy/integrators/sde/common.py create mode 100644 brainpy/integrators/sde/euler_and_milstein.py create mode 100644 brainpy/integrators/sde/exp_euler.py create mode 100644 brainpy/integrators/sde/srk_scalar.py create mode 100644 brainpy/integrators/sde/srk_strong.py rename brainpy/{integration/utils.py => integrators/sympy_analysis.py} (57%) create mode 100644 brainpy/integrators/utils.py create mode 100644 brainpy/simulation/__init__.py create mode 100644 brainpy/simulation/constants.py create mode 100644 brainpy/simulation/monitors.py create mode 100644 brainpy/simulation/network.py create mode 100644 brainpy/simulation/population.py create mode 100644 brainpy/simulation/runner.py create mode 100644 brainpy/simulation/utils.py delete mode 100644 brainpy/tools/functions.py create mode 100644 examples/FitzHugh_Nagumo.py create mode 100644 examples/hh_numba_cpu.py create mode 100644 tests/backend/__init__.py create mode 100644 tests/backend/runners/__init__.py create mode 100644 tests/backend/runners/numba_runner.py create mode 100644 tests/integrators/__init__.py create mode 100644 tests/integrators/test_ast_analysis.py create mode 100644 tests/integrators/test_ode_adaptive_rk.py create mode 100644 tests/integrators/test_ode_rk.py create mode 100644 tests/integrators/test_sde_scalar.py create mode 100644 tests/test_ode_adaptive_rk.py create mode 100644 tests/test_ode_rk.py diff --git a/.gitignore b/.gitignore index ee712121..1421a819 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ publishment.md TODO.md .vscode -examples/ develop/benchmark/COBA/results develop/outputdir diff --git a/brainpy/__init__.py b/brainpy/__init__.py index 6e99d0d7..61c44cbd 100644 --- a/brainpy/__init__.py +++ b/brainpy/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__version__ = "0.3.6" +__version__ = "1.0.0-rc1" # "profile" module from . import profile @@ -8,38 +8,35 @@ from . import profile # "backend" module from . import backend +# "integrators" module +from . import integrators +from .integrators import ode +from .integrators import sde +from .integrators.integrate_wrapper import * +from .integrators.delay_vars import ConstantDelay +from .integrators.delay_vars import VaryingDelay +from .integrators.delay_vars import NeutralDelay + +# "simulation" module +from . import simulation as core +from .simulation.population import Population +from .simulation.population import NeuGroup +from .simulation.population import TwoEndConn +from .simulation.network import Network + # "connectivity" module from . import connectivity from . import connectivity as connect -# "core" module -from . import core as core -from .core.base import ObjType -from .core.base import Ensemble -from .core.neurons import NeuType -from .core.neurons import NeuGroup -from .core.synapses import SynType -from .core.synapses import SynConn -from .core.synapses import delayed -from .core.network import Network -from .core import types -from .core.types import ObjState -from .core.types import NeuState -from .core.types import SynState - -# "integration" module -from . import integration -from .integration import integrate - # "analysis" module from . import analysis -# "tools" module -from . import tools - # "visualization" module from . import visualization as visualize +# "tools" module +from . import tools + # other modules from . import inputs from . import measure diff --git a/brainpy/analysis/__init__.py b/brainpy/analysis/__init__.py index abf48228..a322388b 100644 --- a/brainpy/analysis/__init__.py +++ b/brainpy/analysis/__init__.py @@ -5,4 +5,3 @@ from .utils import * from .base import * from .phase_plane import * from .bifurcation import * - diff --git a/brainpy/analysis/base.py b/brainpy/analysis/base.py index 6fe3dc08..445e7537 100644 --- a/brainpy/analysis/base.py +++ b/brainpy/analysis/base.py @@ -6,12 +6,13 @@ from copy import deepcopy import numpy as np import sympy -from . import solver -from . import utils -from .. import core -from .. import errors -from .. import integration -from .. import tools +from brainpy import simulation +from brainpy import errors +from brainpy import integrators +from brainpy import tools +from brainpy.analysis import solver +from brainpy.analysis import utils + __all__ = [ 'BaseNeuronAnalyzer', @@ -35,8 +36,9 @@ class BaseNeuronAnalyzer(object): Parameters ---------- - model : core.NeuType - The neuronal type model. + model_or_intgs : simulation.Population, function, functions + A model of the population, the integrator function, + or a list/tuple of integrator functions. target_vars : dict The target/dynamical variables. fixed_vars : dict @@ -71,7 +73,7 @@ class BaseNeuronAnalyzer(object): """ def __init__(self, - model, + model_or_intgs, target_vars, fixed_vars=None, target_pars=None, @@ -80,11 +82,11 @@ class BaseNeuronAnalyzer(object): options=None): # model - # ------ - if not isinstance(model, core.NeuType): - raise errors.ModelUseError(f'Neuron Dynamics Analyzer now only support NeuType, ' - f'but get {type(model)}.') - self.model = model + # ----- + if not isinstance(model_or_intgs, simulation.Population): + raise errors.ModelUseError(f'Neuron Dynamics Analyzer now only support Population, ' + f'but get {type(model_or_intgs)}.') + self.model = model_or_intgs # target variables # ---------------- @@ -105,20 +107,20 @@ class BaseNeuronAnalyzer(object): raise errors.ModelUseError('"fixed_vars" must be a dict with the format ' 'of {"var1": val1, "var2": val2}.') self.fixed_vars = dict() - for integrator in model.integrators: + for integrator in model_or_intgs.integrators: var_name = integrator.diff_eq.var_name if var_name not in target_vars: if var_name in fixed_vars: self.fixed_vars[var_name] = fixed_vars.get(var_name) else: - self.fixed_vars[var_name] = model.variables.get(var_name) + self.fixed_vars[var_name] = model_or_intgs.variables.get(var_name) for key in fixed_vars.keys(): if key not in self.fixed_vars: self.fixed_vars[key] = fixed_vars.get(key) # equations of dynamical variables # -------------------------------- - var2eq = {integrator.diff_eq.var_name: integrator for integrator in model.integrators} + var2eq = {integrator.diff_eq.var_name: integrator for integrator in model_or_intgs.integrators} target_func_args = set() self.target_eqs = tools.DictPlus() for key in self.target_vars.keys(): @@ -142,9 +144,9 @@ class BaseNeuronAnalyzer(object): raise errors.ModelUseError('"pars_update" must be a dict with the format ' 'of {"par1": val1, "par2": val2}.') for key in pars_update.keys(): - if key not in model.step_scopes: + if key not in model_or_intgs.step_scopes: if key not in target_func_args: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model.name}" model.') + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model.') self.pars_update = pars_update # dynamical parameters @@ -155,9 +157,9 @@ class BaseNeuronAnalyzer(object): raise errors.ModelUseError('"pars_dynamical" must be a dict with the format ' 'of {"par1": (val1, val2)}.') for key in target_pars.keys(): - if key not in model.step_scopes: + if key not in model_or_intgs.step_scopes: if key not in target_func_args: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model.name}" model.') + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model.') self.target_pars = target_pars if isinstance(self.target_vars, OrderedDict): self.dpar_names = list(self.target_pars.keys()) @@ -242,7 +244,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): if 'dxdt' not in self.analyzed_results: scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integration.get_mapping_scope()) + scope.update(integrators.get_mapping_scope()) scope.update(self.x_eq_group.diff_eq.func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) func_code = f'def func({argument}):\n' @@ -260,11 +262,11 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): x_var = self.dvar_names[0] x_symbol = sympy.Symbol(x_var, real=True) x_eq = self.x_eq_group.sub_exprs[-1].code - x_eq = integration.str2sympy(x_eq) + x_eq = integrators.str2sympy(x_eq) eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integration.get_mapping_scope()) + eq_x_scope.update(integrators.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -282,7 +284,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): # check all_vars = set(eq_x_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integration.sympy2str(dfxdx_expr), all_vars): + if utils.contain_unknown_symbol(integrators.sympy2str(dfxdx_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -290,7 +292,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): func_codes = [f'def dfdx({argument}):'] for expr in self.x_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integration.sympy2str(dfxdx_expr)}') + func_codes.append(f'return {integrators.sympy2str(dfxdx_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_x_scope) dfdx = eq_x_scope['dfdx'] sympy_failed = False @@ -320,11 +322,11 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): """ if 'fixed_point' not in self.analyzed_results: - x_eq = integration.str2sympy(self.x_eq_group.sub_exprs[-1].code) + x_eq = integrators.str2sympy(self.x_eq_group.sub_exprs[-1].code) scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integration.get_mapping_scope()) + scope.update(integrators.get_mapping_scope()) scope.update(self.x_eq_group.diff_eq.func_scope) scope['numpy'] = np @@ -345,7 +347,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): for res in results: all_vars = set(scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integration.sympy2str(res), all_vars): + if utils.contain_unknown_symbol(integrators.sympy2str(res), all_vars): print('failed because contain unknown symbols.') sympy_failed = True break @@ -355,7 +357,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): func_codes = [f'def solve_x({argument2}):'] for expr in self.x_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - result_expr = ', '.join([integration.sympy2str(expr) for expr in results]) + result_expr = ', '.join([integrators.sympy2str(expr) for expr in results]) func_codes.append(f'_res_ = {result_expr}') func_codes.append(f'return np.array(_res_)') @@ -454,7 +456,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check "f" scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integration.get_mapping_scope()) + scope.update(integrators.get_mapping_scope()) if a.endswith('y_eq'): scope.update(self.y_eq_group['diff_eq'].func_scope) else: @@ -485,7 +487,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_var = self.dvar_names[1] scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integration.get_mapping_scope()) + scope.update(integrators.get_mapping_scope()) scope.update(self.y_eq_group.diff_eq.func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) func_code = f'def func({argument}):\n' @@ -503,11 +505,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_var = self.dvar_names[1] y_symbol = sympy.Symbol(y_var, real=True) x_eq = self.target_eqs[x_var].sub_exprs[-1].code - x_eq = integration.str2sympy(x_eq) + x_eq = integrators.str2sympy(x_eq) eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integration.get_mapping_scope()) + eq_x_scope.update(integrators.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -525,7 +527,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check all_vars = set(eq_x_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integration.sympy2str(dfxdy_expr), all_vars): + if utils.contain_unknown_symbol(integrators.sympy2str(dfxdy_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -533,7 +535,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): func_codes = [f'def dfdy({argument}):'] for expr in self.x_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integration.sympy2str(dfxdy_expr)}') + func_codes.append(f'return {integrators.sympy2str(dfxdy_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_x_scope) dfdy = eq_x_scope['dfdy'] sympy_failed = False @@ -566,11 +568,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): x_symbol = sympy.Symbol(x_var, real=True) y_var = self.dvar_names[1] y_eq = self.target_eqs[y_var].sub_exprs[-1].code - y_eq = integration.str2sympy(y_eq) + y_eq = integrators.str2sympy(y_eq) eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integration.get_mapping_scope()) + eq_y_scope.update(integrators.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -588,7 +590,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check all_vars = set(eq_y_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integration.sympy2str(dfydx_expr), all_vars): + if utils.contain_unknown_symbol(integrators.sympy2str(dfydx_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -596,7 +598,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): func_codes = [f'def dgdx({argument}):'] for expr in self.y_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integration.sympy2str(dfydx_expr)}') + func_codes.append(f'return {integrators.sympy2str(dfydx_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_y_scope) dgdx = eq_y_scope['dgdx'] sympy_failed = False @@ -629,11 +631,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_var = self.dvar_names[1] y_symbol = sympy.Symbol(y_var, real=True) y_eq = self.target_eqs[y_var].sub_exprs[-1].code - y_eq = integration.str2sympy(y_eq) + y_eq = integrators.str2sympy(y_eq) eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integration.get_mapping_scope()) + eq_y_scope.update(integrators.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -652,7 +654,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check all_vars = set(eq_y_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integration.sympy2str(dfydx_expr), all_vars): + if utils.contain_unknown_symbol(integrators.sympy2str(dfydx_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -660,7 +662,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): func_codes = [f'def dgdy({argument}):'] for expr in self.y_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integration.sympy2str(dfydx_expr)}') + func_codes.append(f'return {integrators.sympy2str(dfydx_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_y_scope) dgdy = eq_y_scope['dgdy'] sympy_failed = False @@ -717,7 +719,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): eq_xy_scope = deepcopy(self.pars_update) eq_xy_scope.update(self.fixed_vars) - eq_xy_scope.update(integration.get_mapping_scope()) + eq_xy_scope.update(integrators.get_mapping_scope()) eq_xy_scope.update(self.x_eq_group['diff_eq'].func_scope) eq_xy_scope.update(self.y_eq_group['diff_eq'].func_scope) @@ -826,7 +828,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # f eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integration.get_mapping_scope()) + eq_x_scope.update(integrators.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) func_codes = [f'def f_x({",".join(self.dvar_names + self.dpar_names)}):'] func_codes.extend([f'{expr.var_name} = {expr.code}' @@ -838,7 +840,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # g eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integration.get_mapping_scope()) + eq_y_scope.update(integrators.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) func_codes = [f'def g_y({",".join(self.dvar_names + self.dpar_names)}):'] func_codes.extend([f'{expr.var_name} = {expr.code}' @@ -893,7 +895,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # x equation scope eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integration.get_mapping_scope()) + eq_x_scope.update(integrators.get_mapping_scope()) eq_x_scope.update(self.x_eq_group.diff_eq.func_scope) argument = ','.join(self.dvar_names[2:] + self.dpar_names) @@ -967,7 +969,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # y equation scope eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integration.get_mapping_scope()) + eq_y_scope.update(integrators.get_mapping_scope()) eq_y_scope.update(self.y_eq_group.diff_eq.func_scope) argument = ','.join(self.dvar_names[2:] + self.dpar_names) @@ -1031,11 +1033,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: y_symbol = sympy.Symbol(self.y_var, real=True) code = self.target_eqs[self.y_var].sub_exprs[-1].code - y_eq = integration.str2sympy(code).expr + y_eq = integrators.str2sympy(code).expr eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integration.get_mapping_scope()) + eq_y_scope.update(integrators.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1051,7 +1053,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_by_x_in_y_eq = f() if len(y_by_x_in_y_eq) > 1: raise NotImplementedError('Do not support multiple values.') - y_by_x_in_y_eq = integration.sympy2str(y_by_x_in_y_eq[0]) + y_by_x_in_y_eq = integrators.sympy2str(y_by_x_in_y_eq[0]) # check all_vars = set(eq_y_scope.keys()) @@ -1105,11 +1107,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: y_symbol = sympy.Symbol(self.y_var, real=True) code = self.x_eq_group.sub_exprs[-1].code - x_eq = integration.str2sympy(code).expr + x_eq = integrators.str2sympy(code).expr eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integration.get_mapping_scope()) + eq_x_scope.update(integrators.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1126,7 +1128,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_by_x_in_x_eq = f() if len(y_by_x_in_x_eq) > 1: raise NotImplementedError('Do not support multiple values.') - y_by_x_in_x_eq = integration.sympy2str(y_by_x_in_x_eq[0]) + y_by_x_in_x_eq = integrators.sympy2str(y_by_x_in_x_eq[0]) all_vars = set(eq_x_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) @@ -1179,11 +1181,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: x_symbol = sympy.Symbol(self.x_var, real=True) code = self.target_eqs[self.y_var].sub_exprs[-1].code - y_eq = integration.str2sympy(code).expr + y_eq = integrators.str2sympy(code).expr eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integration.get_mapping_scope()) + eq_y_scope.update(integrators.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1198,7 +1200,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): x_by_y_in_y_eq = f() if len(x_by_y_in_y_eq) > 1: raise NotImplementedError('Do not support multiple values.') - x_by_y_in_y_eq = integration.sympy2str(x_by_y_in_y_eq[0]) + x_by_y_in_y_eq = integrators.sympy2str(x_by_y_in_y_eq[0]) # check all_vars = set(eq_y_scope.keys()) @@ -1252,11 +1254,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: x_symbol = sympy.Symbol(self.x_var, real=True) code = self.x_eq_group.sub_exprs[-1].code - x_eq = integration.str2sympy(code).expr + x_eq = integrators.str2sympy(code).expr eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integration.get_mapping_scope()) + eq_x_scope.update(integrators.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1271,7 +1273,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): x_by_y_in_x_eq = f() if len(x_by_y_in_x_eq) > 1: raise NotImplementedError('Do not support multiple values.') - x_by_y_in_x_eq = integration.sympy2str(x_by_y_in_x_eq[0]) + x_by_y_in_x_eq = integrators.sympy2str(x_by_y_in_x_eq[0]) # check all_vars = set(eq_x_scope.keys()) diff --git a/brainpy/analysis/bifurcation.py b/brainpy/analysis/bifurcation.py index 00f77cef..8b003d22 100644 --- a/brainpy/analysis/bifurcation.py +++ b/brainpy/analysis/bifurcation.py @@ -7,11 +7,12 @@ import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.mplot3d import Axes3D -from . import base -from . import utils -from .. import core -from .. import errors -from .. import profile +from brainpy import simulation +from brainpy import errors +from brainpy import profile +from brainpy.analysis import base +from brainpy.analysis import stability +from brainpy.analysis import utils __all__ = [ 'Bifurcation', @@ -37,18 +38,18 @@ class Bifurcation(object): Parameters ---------- - model : NeuType + model_or_intgs : NeuType An abstract neuronal type defined in BrainPy. """ - def __init__(self, model, target_pars, target_vars, fixed_vars=None, pars_update=None, + def __init__(self, model_or_intgs, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): # check "model" - if not isinstance(model, core.NeuType): + if not isinstance(model_or_intgs, simulation.Population): raise errors.ModelUseError('Bifurcation analysis only support neuron type model.') - self.model = model + self.model = model_or_intgs # check "target_pars" if not isinstance(target_pars, dict): @@ -81,13 +82,13 @@ class Bifurcation(object): raise errors.ModelUseError('"pars_update" must be a dict the format of: ' '{"Par A": A_value, "Par B": B_value}') for key in pars_update.keys(): - if key not in model.step_scopes: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model.name}" model. ') + if key not in model_or_intgs.step_scopes: + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model. ') self.pars_update = pars_update # bifurcation analysis if len(self.target_vars) == 1: - self.analyzer = _Bifurcation1D(model=model, + self.analyzer = _Bifurcation1D(model_or_intgs=model_or_intgs, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -96,7 +97,7 @@ class Bifurcation(object): options=options) elif len(self.target_vars) == 2: - self.analyzer = _Bifurcation2D(model=model, + self.analyzer = _Bifurcation2D(model_or_intgs=model_or_intgs, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -117,9 +118,9 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): Using this class, we can make co-dimension1 or co-dimension2 bifurcation analysis. """ - def __init__(self, model, target_pars, target_vars, fixed_vars=None, + def __init__(self, model_or_intgs, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_Bifurcation1D, self).__init__(model=model, + super(_Bifurcation1D, self).__init__(model=model_or_intgs, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -134,7 +135,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): f_dfdx = self.get_f_dfdx() if len(self.target_pars) == 1: - container = {c: {'p': [], 'x': []} for c in utils.get_1d_classification()} + container = {c: {'p': [], 'x': []} for c in stability.get_1d_classification()} # fixed point par_a = self.dpar_names[0] @@ -142,7 +143,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): xs = f_fixed_point(p) for x in xs: dfdx = f_dfdx(x, p) - fp_type = utils.stability_analysis(dfdx) + fp_type = stability.stability_analysis(dfdx) container[fp_type]['p'].append(p) container[fp_type]['x'].append(x) @@ -164,7 +165,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): plt.show() elif len(self.target_pars) == 2: - container = {c: {'p0': [], 'p1': [], 'x': []} for c in utils.get_1d_classification()} + container = {c: {'p0': [], 'p1': [], 'x': []} for c in stability.get_1d_classification()} # fixed point for p0 in self.resolutions[self.dpar_names[0]]: @@ -172,7 +173,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): xs = f_fixed_point(p0, p1) for x in xs: dfdx = f_dfdx(x, p0, p1) - fp_type = utils.stability_analysis(dfdx) + fp_type = stability.stability_analysis(dfdx) container[fp_type]['p0'].append(p0) container[fp_type]['p1'].append(p1) container[fp_type]['x'].append(x) @@ -218,9 +219,9 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): Using this class, we can make co-dimension1 or co-dimension2 bifurcation analysis. """ - def __init__(self, model, target_pars, target_vars, fixed_vars=None, + def __init__(self, model_or_intgs, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_Bifurcation2D, self).__init__(model=model, + super(_Bifurcation2D, self).__init__(model=model_or_intgs, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -240,14 +241,14 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): # bifurcation analysis of co-dimension 1 if len(self.target_pars) == 1: container = {c: {'p': [], self.x_var: [], self.y_var: []} - for c in utils.get_2d_classification()} + for c in stability.get_2d_classification()} # fixed point for p in self.resolutions[self.dpar_names[0]]: xs, ys = f_fixed_point(p) for x, y in zip(xs, ys): dfdx = f_jacobian(x, y, p) - fp_type = utils.stability_analysis(dfdx) + fp_type = stability.stability_analysis(dfdx) container[fp_type]['p'].append(p) container[fp_type][self.x_var].append(x) container[fp_type][self.y_var].append(y) @@ -273,7 +274,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): # bifurcation analysis of co-dimension 2 elif len(self.target_pars) == 2: container = {c: {'p0': [], 'p1': [], self.x_var: [], self.y_var: []} - for c in utils.get_2d_classification()} + for c in stability.get_2d_classification()} # fixed point for p0 in self.resolutions[self.dpar_names[0]]: @@ -281,7 +282,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): xs, ys = f_fixed_point(p0, p1) for x, y in zip(xs, ys): dfdx = f_jacobian(x, y, p0, p1) - fp_type = utils.stability_analysis(dfdx) + fp_type = stability.stability_analysis(dfdx) container[fp_type]['p0'].append(p0) container[fp_type]['p1'].append(p1) container[fp_type][self.x_var].append(x) @@ -334,7 +335,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): all_xs, all_ys, all_p0, all_p1 = [], [], [], [] # unstable node - unstable_node = self.fixed_points[utils._2D_UNSTABLE_NODE] + unstable_node = self.fixed_points[stability.UNSTABLE_NODE_2D] all_xs.extend(unstable_node[self.x_var]) all_ys.extend(unstable_node[self.y_var]) if len(self.dpar_names) == 1: @@ -346,7 +347,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): raise ValueError # unstable focus - unstable_focus = self.fixed_points[utils._2D_UNSTABLE_FOCUS] + unstable_focus = self.fixed_points[stability.UNSTABLE_FOCUS_2D] all_xs.extend(unstable_focus[self.x_var]) all_ys.extend(unstable_focus[self.y_var]) if len(self.dpar_names) == 1: @@ -373,10 +374,10 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): # initialize neuron group length = all_xs.shape[0] - group = core.NeuGroup(self.model, - geometry=length, - monitors=self.dvar_names, - pars_update=self.pars_update) + group = simulation.NeuGroup(self.model, + size=length, + monitors=self.dvar_names, + pars_update=self.pars_update) # group initial state group.ST[self.x_var] = all_xs @@ -386,9 +387,9 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): group.ST[key] = val # run neuron group - group.runner = core.TrajectoryRunner(group, - target_vars=self.dvar_names, - fixed_vars=fixed_vars) + group.runner = simulation.TrajectoryNumbaRunner(group, + target_vars=self.dvar_names, + fixed_vars=fixed_vars) group.run(duration=duration, inputs=inputs) # find limit cycles @@ -464,12 +465,12 @@ class FastSlowBifurcation(object): """ - def __init__(self, model, fast_vars, slow_vars, fixed_vars=None, + def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): # check "model" - if not isinstance(model, core.NeuType): + if not isinstance(model_or_intgs, simulation.NeuType): raise errors.ModelUseError('FastSlowBifurcation only support neuron type model.') - self.model = model + self.model = model_or_intgs # check "fast_vars" if not isinstance(fast_vars, dict): @@ -506,13 +507,13 @@ class FastSlowBifurcation(object): raise errors.ModelUseError('"pars_update" must be a dict the format of: ' '{"Par A": A_value, "Par B": B_value}') for key in pars_update.keys(): - if key not in model.step_scopes: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model.name}" model. ') + if key not in model_or_intgs.step_scopes: + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model. ') self.pars_update = pars_update # bifurcation analysis if len(self.fast_vars) == 1: - self.analyzer = _FastSlow1D(model=model, + self.analyzer = _FastSlow1D(model_or_intgs=model_or_intgs, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, @@ -521,7 +522,7 @@ class FastSlowBifurcation(object): options=options) elif len(self.fast_vars) == 2: - self.analyzer = _FastSlow2D(model=model, + self.analyzer = _FastSlow2D(model_or_intgs=model_or_intgs, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, @@ -543,9 +544,9 @@ class FastSlowBifurcation(object): class _FastSlowTrajectory(object): - def __init__(self, model, fast_vars, slow_vars, fixed_vars=None, + def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, **kwargs): - self.model = model + self.model = model_or_intgs self.fast_vars = fast_vars self.slow_vars = slow_vars self.fixed_vars = fixed_vars @@ -569,16 +570,16 @@ class _FastSlowTrajectory(object): # cannot update dynamical parameters all_vars = self.fast_var_names + self.slow_var_names - self.traj_group = core.NeuGroup(model, - geometry=1, - monitors=all_vars, - pars_update=pars_update) - self.traj_group.runner = core.TrajectoryRunner(self.traj_group, - target_vars=all_vars, - fixed_vars=fixed_vars) + self.traj_group = simulation.NeuGroup(model_or_intgs, + size=1, + monitors=all_vars, + pars_update=pars_update) + self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, + target_vars=all_vars, + fixed_vars=fixed_vars) self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() if not key.startswith('_')} - self.traj_net = core.Network(self.traj_group) + self.traj_net = simulation.Network(self.traj_group) def plot_trajectory(self, initials, duration, plot_duration=None, inputs=(), show=False): """Plot trajectories according to the settings. @@ -725,18 +726,17 @@ class _FastSlowTrajectory(object): plt.show() - class _FastSlow1D(_Bifurcation1D): - def __init__(self, model, fast_vars, slow_vars, fixed_vars=None, + def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_FastSlow1D, self).__init__(model=model, + super(_FastSlow1D, self).__init__(model_or_intgs=model_or_intgs, target_pars=slow_vars, target_vars=fast_vars, fixed_vars=fixed_vars, pars_update=pars_update, numerical_resolution=numerical_resolution, options=options) - self.traj = _FastSlowTrajectory(model=model, + self.traj = _FastSlowTrajectory(model_or_intgs=model_or_intgs, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, @@ -755,16 +755,16 @@ class _FastSlow1D(_Bifurcation1D): class _FastSlow2D(_Bifurcation2D): - def __init__(self, model, fast_vars, slow_vars, fixed_vars=None, + def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_FastSlow2D, self).__init__(model=model, + super(_FastSlow2D, self).__init__(model_or_intgs=model_or_intgs, target_pars=slow_vars, target_vars=fast_vars, fixed_vars=fixed_vars, pars_update=pars_update, numerical_resolution=numerical_resolution, options=options) - self.traj = _FastSlowTrajectory(model=model, + self.traj = _FastSlowTrajectory(model_or_intgs=model_or_intgs, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, diff --git a/brainpy/analysis/phase_plane.py b/brainpy/analysis/phase_plane.py index 6680fe17..4f276a94 100644 --- a/brainpy/analysis/phase_plane.py +++ b/brainpy/analysis/phase_plane.py @@ -3,11 +3,11 @@ import matplotlib.pyplot as plt import numpy as np -from . import base -from . import utils -from .. import core -from .. import errors -from .. import profile +from brainpy import simulation +from brainpy import errors +from brainpy import profile +from brainpy.analysis import base +from brainpy.analysis import utils __all__ = [ 'PhasePlane', @@ -26,7 +26,7 @@ class PhasePlane(object): Parameters ---------- - model : NeuType + model_or_intgs : NeuType The neuron model which defines the differential equations by using `brainpy.integrate`. target_vars : dict @@ -76,7 +76,7 @@ class PhasePlane(object): def __init__( self, - model, + model_or_intgs, target_vars, fixed_vars=None, pars_update=None, @@ -85,9 +85,9 @@ class PhasePlane(object): ): # check "model" - if not isinstance(model, core.NeuType): + if not isinstance(model_or_intgs, simulation.Population): raise errors.ModelUseError('Phase plane analysis only support neuron type model.') - self.model = model + self.model = model_or_intgs # check "target_vars" if not isinstance(target_vars, dict): @@ -110,20 +110,20 @@ class PhasePlane(object): raise errors.ModelUseError('"pars_update" must be a dict with the format of: ' '{"Par A": A_value, "Par B": B_value}') for key in pars_update.keys(): - if key not in model.step_scopes: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model.name}" model.') + if key not in model_or_intgs.step_scopes: + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model.') self.pars_update = pars_update # analyzer if len(target_vars) == 1: - self.analyzer = _PhasePlane1D(model=model, + self.analyzer = _PhasePlane1D(model=model_or_intgs, target_vars=target_vars, fixed_vars=fixed_vars, pars_update=pars_update, numerical_resolution=numerical_resolution, options=options) elif len(target_vars) == 2: - self.analyzer = _PhasePlane2D(model=model, + self.analyzer = _PhasePlane2D(model=model_or_intgs, target_vars=target_vars, fixed_vars=fixed_vars, pars_update=pars_update, @@ -261,16 +261,16 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): # --------------------- # cannot update dynamical parameters - self.traj_group = core.NeuGroup(self.model, - geometry=1, - monitors=self.dvar_names, - pars_update=self.pars_update) - self.traj_group.runner = core.TrajectoryRunner(self.traj_group, - target_vars=self.dvar_names, - fixed_vars=self.fixed_vars) + self.traj_group = simulation.NeuGroup(self.model, + size=1, + monitors=self.dvar_names, + pars_update=self.pars_update) + self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, + target_vars=self.dvar_names, + fixed_vars=self.fixed_vars) self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() if not key.startswith('_')} - self.traj_net = core.Network(self.traj_group) + self.traj_net = simulation.Network(self.traj_group) def plot_vector_field(self, plot_method='streamplot', plot_style=None, show=False): """Plot the vector field. diff --git a/brainpy/analysis/solver.py b/brainpy/analysis/solver.py index 4ce67594..31e89956 100644 --- a/brainpy/analysis/solver.py +++ b/brainpy/analysis/solver.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- from collections import namedtuple +from importlib import import_module -import numba as nb import numpy as np -from scipy.optimize import shgo + +try: + numba = import_module('numba') +except ModuleNotFoundError: + numba = None __all__ = [ 'brentq', @@ -18,7 +22,6 @@ _ECONVERR = -1 results = namedtuple('results', ['root', 'function_calls', 'iterations', 'converged']) -@nb.njit def brentq(f, a, b, args=(), xtol=2e-14, maxiter=200, rtol=4 * np.finfo(float).eps): """ Find a root of a function in a bracketing interval using Brent's method @@ -147,11 +150,13 @@ def brentq(f, a, b, args=(), xtol=2e-14, maxiter=200, rtol=4 * np.finfo(float).e raise RuntimeError("Failed to converge") # x, funcalls, iterations = root, funcalls, itr - return root, funcalls, itr -@nb.njit +if numba is not None: + brentq = numba.njit(brentq) + + def find_root_of_1d(f, f_points, args=(), tol=1e-8): """Find the roots of the given function by numerical methods. @@ -198,6 +203,10 @@ def find_root_of_1d(f, f_points, args=(), tol=1e-8): return roots +if numba is not None: + find_root_of_1d = numba.njit(find_root_of_1d) + + def find_root_of_2d(f, x_bound, y_bound, args=(), shgo_args=None, fl_tol=1e-6, xl_tol=1e-4, verbose=False): """Find the root of a two dimensional function. @@ -248,6 +257,7 @@ def find_root_of_2d(f, x_bound, y_bound, args=(), shgo_args=None, res : tuple The roots. """ + from scipy.optimize import shgo # 1. shgo arguments if shgo_args is None: diff --git a/brainpy/analysis/stability.py b/brainpy/analysis/stability.py new file mode 100644 index 00000000..ae9eb663 --- /dev/null +++ b/brainpy/analysis/stability.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +import numpy as np + + +CENTER_MANIFOLD = 'center manifold' +SADDLE_NODE = 'saddle node' +STABLE_POINT_1D = 'stable point' +UNSTABLE_POINT_1D = 'unstable point' +CENTER_2D = 'center' +STABLE_NODE_2D = 'stable node' +STABLE_FOCUS_2D = 'stable focus' +STABLE_STAR_2D = 'stable star' +STABLE_DEGENERATE_2D = 'stable degenerate' +UNSTABLE_NODE_2D = 'unstable node' +UNSTABLE_FOCUS_2D = 'unstable focus' +UNSTABLE_STAR_2D = 'unstable star' +UNSTABLE_DEGENERATE_2D = 'unstable degenerate' +UNSTABLE_LINE_2D = 'unstable line' + + +def get_1d_classification(): + return [SADDLE_NODE, STABLE_POINT_1D, UNSTABLE_POINT_1D] + + +def get_2d_classification(): + return [SADDLE_NODE, CENTER_2D, STABLE_NODE_2D, STABLE_FOCUS_2D, + STABLE_STAR_2D, CENTER_MANIFOLD, UNSTABLE_NODE_2D, + UNSTABLE_FOCUS_2D, UNSTABLE_STAR_2D, UNSTABLE_LINE_2D, + STABLE_DEGENERATE_2D, UNSTABLE_DEGENERATE_2D] + + +def get_3d_classification(): + return [] + + +def stability_analysis(derivative): + """Stability analysis for fixed point [1]_. + + Parameters + ---------- + derivative : float, tuple, list, np.ndarray + The derivative of the f. + + Returns + ------- + fp_type : str + The type of the fixed point. + + References + ---------- + + .. [1] http://www.egwald.ca/nonlineardynamics/twodimensionaldynamics.php + + """ + if np.size(derivative) == 1: + if derivative == 0: + return SADDLE_NODE + elif derivative > 0: + return STABLE_POINT_1D + else: + return UNSTABLE_POINT_1D + + elif np.size(derivative) == 4: + a = derivative[0][0] + b = derivative[0][1] + c = derivative[1][0] + d = derivative[1][1] + + # trace + p = a + d + # det + q = a * d - b * c + + # judgement + if q < 0: + return SADDLE_NODE + elif q == 0: + if p <= 0: + return CENTER_MANIFOLD + else: + return UNSTABLE_LINE_2D + else: + # parabola + e = p * p - 4 * q + if p == 0: + return CENTER_2D + elif p > 0: + if e < 0: + return UNSTABLE_FOCUS_2D + elif e > 0: + return UNSTABLE_NODE_2D + else: + w = np.linalg.eigvals(derivative) + if w[0] == w[1]: + return UNSTABLE_DEGENERATE_2D + else: + return UNSTABLE_STAR_2D + else: + if e < 0: + return STABLE_FOCUS_2D + elif e > 0: + return STABLE_NODE_2D + else: + w = np.linalg.eigvals(derivative) + if w[0] == w[1]: + return STABLE_DEGENERATE_2D + else: + return STABLE_STAR_2D + + elif np.size(derivative) == 9: + pass + + else: + raise ValueError('Unknown derivatives, only supports the jacobian ' + 'matrix with the shape of(1), (2, 2), or (3, 3).') diff --git a/brainpy/analysis/utils.py b/brainpy/analysis/utils.py index 8735c16b..ed6798ef 100644 --- a/brainpy/analysis/utils.py +++ b/brainpy/analysis/utils.py @@ -5,14 +5,20 @@ import inspect import threading import numpy as np -from numba import njit -from numba.core.dispatcher import Dispatcher -from .. import backend -from .. import tools +from brainpy import backend +from brainpy import tools +from . import stability + +try: + import numba + from numba.core.dispatcher import Dispatcher +except ModuleNotFoundError: + numba = None + Dispatcher = None + __all__ = [ - 'stability_analysis', 'rescale', 'timeout', 'jit_compile', @@ -20,138 +26,33 @@ __all__ = [ 'contain_unknown_symbol', ] -_CENTER_MANIFOLD = 'center manifold' -_SADDLE_NODE = 'saddle node' -_1D_STABLE_POINT = 'stable point' -_1D_UNSTABLE_POINT = 'unstable point' -_2D_CENTER = 'center' -_2D_STABLE_NODE = 'stable node' -_2D_STABLE_FOCUS = 'stable focus' -_2D_STABLE_STAR = 'stable star' -_2D_STABLE_DEGENERATE = 'stable degenerate' -# _2D_STABLE_LINE = 'stable line' -_2D_UNSTABLE_NODE = 'unstable node' -_2D_UNSTABLE_FOCUS = 'unstable focus' -_2D_UNSTABLE_STAR = 'unstable star' -_2D_UNSTABLE_DEGENERATE = 'unstable degenerate' -_2D_UNSTABLE_LINE = 'unstable line' - plot_scheme = { - _1D_STABLE_POINT: {"color": 'tab:red'}, - _2D_STABLE_NODE: {"color": 'tab:red'}, + stability.STABLE_POINT_1D: {"color": 'tab:red'}, + stability.STABLE_NODE_2D: {"color": 'tab:red'}, - _1D_UNSTABLE_POINT: {"color": 'tab:olive'}, - _2D_UNSTABLE_NODE: {"color": 'tab:olive'}, + stability.UNSTABLE_POINT_1D: {"color": 'tab:olive'}, + stability.UNSTABLE_NODE_2D: {"color": 'tab:olive'}, - _2D_STABLE_FOCUS: {"color": 'tab:purple'}, - _2D_UNSTABLE_FOCUS: {"color": 'tab:cyan'}, + stability.STABLE_FOCUS_2D: {"color": 'tab:purple'}, + stability.UNSTABLE_FOCUS_2D: {"color": 'tab:cyan'}, - _SADDLE_NODE: {"color": 'tab:blue'}, - _2D_CENTER: {'color': 'lime'}, - # _2D_UNIFORM_MOTION: {'color': 'red'}, + stability.SADDLE_NODE: {"color": 'tab:blue'}, + stability.CENTER_2D: {'color': 'lime'}, + # stability._2D_UNIFORM_MOTION: {'color': 'red'}, - _CENTER_MANIFOLD: {'color': 'orangered'}, - _2D_UNSTABLE_LINE: {'color': 'dodgerblue'}, + stability.CENTER_MANIFOLD: {'color': 'orangered'}, + stability.UNSTABLE_LINE_2D: {'color': 'dodgerblue'}, - _2D_UNSTABLE_STAR: {'color': 'green'}, - _2D_STABLE_STAR: {'color': 'orange'}, + stability.UNSTABLE_STAR_2D: {'color': 'green'}, + stability.STABLE_STAR_2D: {'color': 'orange'}, - _2D_UNSTABLE_DEGENERATE: {'color': 'springgreen'}, - _2D_STABLE_DEGENERATE: {'color': 'blueviolet'}, + stability.UNSTABLE_DEGENERATE_2D: {'color': 'springgreen'}, + stability.STABLE_DEGENERATE_2D: {'color': 'blueviolet'}, } -def get_1d_classification(): - return [_SADDLE_NODE, _1D_STABLE_POINT, _1D_UNSTABLE_POINT] - - -def get_2d_classification(): - return [_SADDLE_NODE, _2D_CENTER, _2D_STABLE_NODE, _2D_STABLE_FOCUS, - _2D_STABLE_STAR, _CENTER_MANIFOLD, _2D_UNSTABLE_NODE, - _2D_UNSTABLE_FOCUS, _2D_UNSTABLE_STAR, _2D_UNSTABLE_LINE, - _2D_STABLE_DEGENERATE, _2D_UNSTABLE_DEGENERATE] - - -def stability_analysis(derivative): - """Stability analysis for fixed point [1]_. - - Parameters - ---------- - derivative : float, tuple, list, np.ndarray - The derivative of the f. - - Returns - ------- - fp_type : str - The type of the fixed point. - - References - ---------- - - .. [1] http://www.egwald.ca/nonlineardynamics/twodimensionaldynamics.php - - """ - if np.size(derivative) == 1: - if derivative == 0: - return _SADDLE_NODE - elif derivative > 0: - return _1D_STABLE_POINT - else: - return _1D_UNSTABLE_POINT - - elif np.size(derivative) == 4: - a = derivative[0][0] - b = derivative[0][1] - c = derivative[1][0] - d = derivative[1][1] - - # trace - p = a + d - # det - q = a * d - b * c - - # judgement - if q < 0: - return _SADDLE_NODE - elif q == 0: - if p <= 0: - return _CENTER_MANIFOLD - else: - return _2D_UNSTABLE_LINE - else: - # parabola - e = p * p - 4 * q - if p == 0: - return _2D_CENTER - elif p > 0: - if e < 0: - return _2D_UNSTABLE_FOCUS - elif e > 0: - return _2D_UNSTABLE_NODE - else: - w = np.linalg.eigvals(derivative) - if w[0] == w[1]: - return _2D_UNSTABLE_DEGENERATE - else: - return _2D_UNSTABLE_STAR - else: - if e < 0: - return _2D_STABLE_FOCUS - elif e > 0: - return _2D_STABLE_NODE - else: - w = np.linalg.eigvals(derivative) - if w[0] == w[1]: - return _2D_STABLE_DEGENERATE - else: - return _2D_STABLE_STAR - - elif np.size(derivative) == 9: - pass - - else: - raise ValueError('Unknown derivatives, only supports the jacobian ' - 'matrixwith the shape of(1), (2, 2), or (3, 3).') +def get_integrators(population): + pass def rescale(min_max, scale=0.01): @@ -214,13 +115,15 @@ def _jit(func): func_code = tools.deindent(tools.get_func_source(func)) exec(compile(func_code, '', "exec"), code_scope) func = code_scope[func.__name__] - return njit(func) + return numba.njit(func) else: - njit(func) + return numba.njit(func) def jit_compile(scope, func_code, func_name): - # get function scope + if numba is None: + return + # get function scope func_scope = dict() for key, val in scope.items(): if callable(val): @@ -234,7 +137,7 @@ def jit_compile(scope, func_code, func_name): # compile function exec(compile(func_code, '', 'exec'), func_scope) - return njit(func_scope[func_name]) + return numba.njit(func_scope[func_name]) def contain_unknown_symbol(expr, scope): @@ -288,7 +191,6 @@ def add_arrow(line, position=None, direction='right', size=15, color=None): size=size) -@njit def f1(arr, grad, tol): condition = np.logical_and(grad[:-1] * grad[1:] <= 0, grad[:-1] >= 0) indexes = np.where(condition)[0] @@ -302,7 +204,10 @@ def f1(arr, grad, tol): return np.array([-1, -1]) -@njit +if numba is not None: + f1 = numba.njit(f1) + + def f2(arr, grad, tol): condition = np.logical_and(grad[:-1] * grad[1:] <= 0, grad[:-1] <= 0) indexes = np.where(condition)[0] @@ -316,6 +221,10 @@ def f2(arr, grad, tol): return np.array([-1, -1]) +if numba is not None: + f2 = numba.njit(f2) + + def find_indexes_of_limit_cycle_max(arr, tol=0.001): grad = np.gradient(arr) return f1(arr, grad, tol) @@ -326,7 +235,6 @@ def find_indexes_of_limit_cycle_min(arr, tol=0.001): return f2(arr, grad, tol) -@njit def _identity(a, b, tol=0.01): if np.abs(a - b) < tol: return True @@ -334,6 +242,10 @@ def _identity(a, b, tol=0.01): return False +if numba is not None: + _identity = numba.njit(_identity) + + def find_indexes_of_limit_cycle_max2(arr, tol=0.001): if np.ndim(arr) == 1: grad = np.gradient(arr) diff --git a/brainpy/backend/__init__.py b/brainpy/backend/__init__.py index 48c9ddfa..10257270 100644 --- a/brainpy/backend/__init__.py +++ b/brainpy/backend/__init__.py @@ -1,4 +1,133 @@ # -*- coding: utf-8 -*- -from . import numpy_overload -from .utils import * +from types import ModuleType + +from brainpy import errors +from .operators.bk_numpy import * + +_backend = 'numpy' # default backend is NumPy +_node_runner = None +_net_runner = None +NEEDED_OPS = ['normal', 'reshape', 'shape', 'exp', 'sum', 'zeros', + 'eye', 'matmul', 'vstack', 'arange'] +SUPPORTED_BACKEND = ['numba', 'numba-parallel', 'numba-cuda', 'jax', + 'numpy', 'pytorch', 'tensorflow', ] +SYSTEM_KEYWORDS = ['_dt', '_t', '_i'] + + +def set(backend, module_or_operations=None, node_runner=None, net_runner=None): + if _backend == backend: + return + + if backend == 'numpy': + from .operators import bk_numpy + from .runners.general_runner import GeneralNodeRunner, GeneralNetRunner + + node_runner = GeneralNodeRunner if node_runner is None else node_runner + net_runner = GeneralNetRunner if net_runner is None else net_runner + module_or_operations = bk_numpy if module_or_operations is None else module_or_operations + + elif backend == 'pytorch': + from .operators import bk_pytorch + from .runners.general_runner import GeneralNodeRunner, GeneralNetRunner + + node_runner = GeneralNodeRunner if node_runner is None else node_runner + net_runner = GeneralNetRunner if net_runner is None else net_runner + module_or_operations = bk_pytorch if module_or_operations is None else module_or_operations + + elif backend == 'tensorflow': + from .operators import bk_tensorflow + from .runners.general_runner import GeneralNodeRunner, GeneralNetRunner + + node_runner = GeneralNodeRunner if node_runner is None else node_runner + net_runner = GeneralNetRunner if net_runner is None else net_runner + module_or_operations = bk_tensorflow if module_or_operations is None else module_or_operations + + elif backend == 'numba': + from .operators import bk_numba_cpu + from .runners.numba_cpu_runner import NumbaCPUNodeRunner, set_numba_profile + + node_runner = NumbaCPUNodeRunner if node_runner is None else node_runner + module_or_operations = bk_numba_cpu if module_or_operations is None else module_or_operations + set_numba_profile(parallel=False) + + elif backend == 'numba-parallel': + from .operators import bk_numba_cpu + from .runners.numba_cpu_runner import NumbaCPUNodeRunner, set_numba_profile + + node_runner = NumbaCPUNodeRunner if node_runner is None else node_runner + module_or_operations = bk_numba_cpu if module_or_operations is None else module_or_operations + set_numba_profile(parallel=True) + + elif backend == 'numba-cuda': + from .operators import bk_numba_cuda + from .runners.numba_cuda_runner import NumbaCudaNodeRunner + + node_runner = NumbaCudaNodeRunner if node_runner is None else node_runner + module_or_operations = bk_numba_cuda if module_or_operations is None else module_or_operations + + elif backend == 'jax': + from .operators import bk_jax + from .runners.jax_runner import JaxRunner + + node_runner = JaxRunner if node_runner is None else node_runner + module_or_operations = bk_jax if module_or_operations is None else module_or_operations + + else: + if module_or_operations is None: + raise errors.ModelUseError(f'Backend "{backend}" is unknown, ' + f'please provide the "module_or_operations" ' + f'to specify the necessary computation units.') + from .runners.general_runner import GeneralNodeRunner + node_runner = GeneralNodeRunner if node_runner is None else node_runner + + global_vars = globals() + global_vars['_backend'] = backend + global_vars['_node_runner'] = node_runner + global_vars['_net_runner'] = net_runner + if isinstance(module_or_operations, ModuleType): + set_ops_from_module(module_or_operations) + elif isinstance(module_or_operations, dict): + set_ops(**module_or_operations) + else: + raise errors.ModelUseError('"module_or_operations" must be a module ' + 'or a dict of operations.') + + +def set_ops_from_module(module): + global_vars = globals() + for ops in NEEDED_OPS: + if not hasattr(module, ops): + raise ValueError(f'Operation "{ops}" is needed, but is not ' + f'defined in module "{module}".') + global_vars[ops] = getattr(module, ops) + + +def set_ops(**kwargs): + global_vars = globals() + for key in global_vars.keys(): + if (not key.startswith('__')) and (key in kwargs): + global_vars[key] = kwargs.pop(key) + + if len(kwargs): + raise ValueError(f'Unknown operations: {list(kwargs.keys())}') + + +def get_backend(): + return _backend + + +def get_node_runner(): + global _node_runner + if _node_runner is None: + from .runners.general_runner import GeneralNodeRunner + _node_runner = GeneralNodeRunner + return _node_runner + + +def get_net_runner(): + global _net_runner + if _net_runner is None: + from .runners.general_runner import GeneralNetRunner + _net_runner = GeneralNetRunner + return _net_runner diff --git a/brainpy/backend/operators/__init__.py b/brainpy/backend/operators/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/brainpy/backend/operators/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/brainpy/backend/operators/bk_jax.py b/brainpy/backend/operators/bk_jax.py new file mode 100644 index 00000000..e77ae3d4 --- /dev/null +++ b/brainpy/backend/operators/bk_jax.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +from jax import numpy +from jax import random + +key = random.PRNGKey(0) + + +def set_seed(seed): + global key + key = random.PRNGKey(seed) + + +def normal(loc, scale, size): + return loc + scale * random.normal(key, shape=size) + + +reshape = numpy.reshape +exp = numpy.exp +sum = numpy.sum +zeros = numpy.zeros +eye = numpy.eye +outer = numpy.outer +dot = numpy.dot +vstack = numpy.vstack +arange = numpy.arange + + +def shape(x): + size = numpy.shape(x) + if len(size) == 0: + return (1,) + else: + return size diff --git a/brainpy/backend/operators/bk_numba_cpu.py b/brainpy/backend/operators/bk_numba_cpu.py new file mode 100644 index 00000000..425fe0c7 --- /dev/null +++ b/brainpy/backend/operators/bk_numba_cpu.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +import numba +import numpy as np + +from . import bk_numba_overload + + +as_tensor = np.asarray +normal = np.random.normal +reshape = np.reshape +exp = np.exp +sum = np.sum +zeros = np.zeros +ones = np.ones +eye = np.eye +outer = np.outer +matmul = np.matmul +vstack = np.vstack +arange = np.arange +shape = np.shape + +# +# @numba.njit +# def shape(x): +# size = np.shape(x) +# if len(size) == 0: +# return (1,) +# else: +# return size + + +@numba.generated_jit(fastmath=True, nopython=True, nogil=True) +def normal_like(x): + if isinstance(x, (numba.types.Integer, numba.types.Float)): + return lambda x: np.random.normal() + else: + return lambda x: np.random.normal(0., 1.0, x.shape) + + +if __name__ == '__main__': + bk_numba_overload diff --git a/brainpy/backend/operators/bk_numba_cuda.py b/brainpy/backend/operators/bk_numba_cuda.py new file mode 100644 index 00000000..bef7c0fa --- /dev/null +++ b/brainpy/backend/operators/bk_numba_cuda.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from numba import cuda + diff --git a/brainpy/backend/numpy_overload.py b/brainpy/backend/operators/bk_numba_overload.py similarity index 87% rename from brainpy/backend/numpy_overload.py rename to brainpy/backend/operators/bk_numba_overload.py index 79d22739..b4181b7e 100644 --- a/brainpy/backend/numpy_overload.py +++ b/brainpy/backend/operators/bk_numba_overload.py @@ -2,10 +2,20 @@ import numba import numpy - from numba.extending import overload +@overload(numpy.shape) +def shape_func(x): + if isinstance(x, (numba.types.Integer, numba.types.Float)): + def shape(x): + return (1,) + + return shape + else: + return numpy.shape + + @overload(numpy.cbrt) def cbrt_func(x): def cbrt(x): @@ -66,10 +76,12 @@ def heaviside_func(x1, x2): @overload(numpy.moveaxis) def moveaxis_func(x, source, destination): def moveaxis(x, source, destination): - shape = list(x.shape) - s = shape.pop(source) - shape.insert(destination, s) - return numpy.transpose(x, tuple(shape)) + axes = list(range(len(x.shape))) + if source < 0: source = axes[source] + if destination < 0: destination = axes[destination] + s = axes.pop(source) + axes.insert(destination, s) + return numpy.transpose(x, tuple(axes)) return moveaxis @@ -91,6 +103,7 @@ def swapaxes_func(x, axis1, axis2): def logspace_func(start, stop, num=50, endpoint=True, base=10.0, dtype=None): def logspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None): return numpy.power(base, numpy.linspace(start, stop, num=num, endpoint=endpoint)).astype(dtype) + return logspace @@ -149,4 +162,3 @@ def average(a, axis=None, weights=None): return numpy.sum(a * weights, axis=axis) / sum(weights) return func - diff --git a/brainpy/backend/operators/bk_numpy.py b/brainpy/backend/operators/bk_numpy.py new file mode 100644 index 00000000..b71e38d6 --- /dev/null +++ b/brainpy/backend/operators/bk_numpy.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +__all__ = [ + 'as_tensor', + 'normal', + 'reshape', + 'shape', + 'exp', + 'sum', + 'zeros', + 'ones', + 'eye', + 'matmul', + 'vstack', + 'arange', + 'moveaxis', +] + +as_tensor = np.asarray +normal = np.random.normal +reshape = np.reshape +exp = np.exp +sum = np.sum +zeros = np.zeros +ones = np.ones +eye = np.eye +matmul = np.matmul +vstack = np.vstack +arange = np.arange +moveaxis = np.moveaxis + + +def shape(x): + size = np.shape(x) + if len(size) == 0: + return (1,) + else: + return size diff --git a/brainpy/backend/operators/bk_pytorch.py b/brainpy/backend/operators/bk_pytorch.py new file mode 100644 index 00000000..1a0528c0 --- /dev/null +++ b/brainpy/backend/operators/bk_pytorch.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +""" +The PyTorch with the version of xx is needed. +""" + + +import torch + +as_tensor = torch.tensor +normal = torch.normal +reshape = torch.reshape +exp = torch.exp +sum = torch.sum +zeros = torch.zeros +ones = torch.ones +eye = torch.eye +outer = torch.outer +dot = torch.mm +vstack = torch.vstack +arange = torch.arange + + +def shape(x): + if isinstance(x, (int, float)): + return (1,) + else: + return x.size() + diff --git a/brainpy/backend/operators/bk_tensorflow.py b/brainpy/backend/operators/bk_tensorflow.py new file mode 100644 index 00000000..19881ba4 --- /dev/null +++ b/brainpy/backend/operators/bk_tensorflow.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + + +""" +The TensorFlow with the version of xx is needed. +""" + +import tensorflow as tf + +reshape = tf.reshape +exp = tf.math.exp +sum = tf.math.reduce_sum +zeros = tf.zeros +eye = tf.eye +dot = tf.matmul +arange = tf.range + + +def outer(A, B): + return tf.tensordot(A, B, axes=0) + + +def vstack(values): + return tf.concat(values, axis=1) + + +def shape(x): + if isinstance(x, (int, float)): + return (1,) + else: + return x.shape() + + +def normal(loc, scale, size): + return tf.random.normal(size, loc, scale) diff --git a/brainpy/backend/operators/standard.py b/brainpy/backend/operators/standard.py new file mode 100644 index 00000000..e7e5574a --- /dev/null +++ b/brainpy/backend/operators/standard.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +""" +In this script, we establish the unified and standard +functions for computation backends. +""" + +import numpy as np + + + +def sum(tensor, axis): + """The sum operation. + + We expect "sum" function will behave like "numpy.sum" + + + + Parameters + ---------- + tensor : array_like + The data to sum. + axis : None or int or tuple of ints, optional + Axis or axes along which a sum is performed. The default, + axis=None, will sum all of the elements of the input array. If + axis is negative it counts from the last to the first axis. + + Returns + ------- + sum_along_axis : ndarray + An array with the same shape as `a`, with the specified + axis removed. If `a` is a 0-d array, or if `axis` is None, a scalar + is returned. If an output array is specified, a reference to + `out` is returned. + + Examples + -------- + >>> sum([0.5, 1.5]) + 2.0 + >>> sum([0.5, 0.7, 0.2, 1.5], dtype=np.int32) + 1 + >>> sum([[0, 1], [0, 5]]) + 6 + >>> sum([[0, 1], [0, 5]], axis=0) + array([0, 6]) + >>> sum([[0, 1], [0, 5]], axis=1) + array([1, 5]) + >>> sum([[0, 1], [np.nan, 5]], where=[False, True], axis=1) + array([1., 5.]) + + If the accumulator is too small, overflow occurs: + + >>> np.ones(128, dtype=np.int8).sum(dtype=np.int8) + -128 + + You can also start the sum with a value other than zero: + + >>> sum([10], initial=5) + 15 + + """ + pass + + + + diff --git a/brainpy/backend/runners/__init__.py b/brainpy/backend/runners/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/brainpy/backend/runners/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/brainpy/backend/runners/general_runner.py b/brainpy/backend/runners/general_runner.py new file mode 100644 index 00000000..6b9cb6bf --- /dev/null +++ b/brainpy/backend/runners/general_runner.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- + +from brainpy import backend +from brainpy import errors +from brainpy.simulation import runner +from . import utils + + +class GeneralNodeRunner(runner.NodeRunner): + """General BrainPy Runner for NumPy, PyTorch, TensorFlow, etc. + """ + + def __init__(self, pop, steps=None): + steps = pop.steps if steps is None else pop.steps + super(GeneralNodeRunner, self).__init__(host=pop, steps=steps) + self.last_inputs = {} + self.formatted_funcs = {} + self.run_func = None + + def get_input_func(self, formatted_inputs, show_code=False): + need_rebuild = False + # check whether the input is changed + # -- + new_inputs = {} + input_keep_same = True + old_input_keys = list(self.last_inputs.keys()) + for key, val, ops, data_type in formatted_inputs: + # set data + self.set_data(self.input_data_name(key), val) + # compare + if key in old_input_keys: + old_input_keys.remove(key) + if backend.shape(self.last_inputs[key][0]) != backend.shape(val): + input_keep_same = False + if show_code: + print(f'The current "{key}" input shape {backend.shape(val)} is different ' + f'from the last input shape {backend.shape(self.last_inputs[key][0])}.') + if self.last_inputs[key][1] != ops: + input_keep_same = False + if show_code: + print(f'The current "{key}" input operation "{ops}" is different ' + f'from the last operation "{self.last_inputs[key][1]}". ') + else: + input_keep_same = False + if show_code: + print(f'The input to a new key "{key}" in {self.host}.') + new_inputs[key] = (val, ops, data_type) + self.last_inputs = new_inputs + if len(old_input_keys): + input_keep_same = False + if show_code: + print(f'The inputs of {old_input_keys} in {self.host} are not provided.') + + # get the function of the input + # --- + if not input_keep_same: + # codes + input_func_name = 'input_step' + host_name = self.host.name + code_scope = {host_name: self.host} + code_lines = [f'def {input_func_name}(_i):'] + for key, val, ops, data_type in formatted_inputs: + if ops == '=': + line = f' {host_name}.{key} = {host_name}.{self.input_data_name(key)}' + else: + line = f' {host_name}.{key} {ops}= {host_name}.{self.input_data_name(key)}' + if data_type == 'iter': + line = line + '[_i]' + code_lines.append(line) + if len(formatted_inputs) == 0: + code_lines.append(' pass') + + # function + code = '\n'.join(code_lines) + if show_code: + print(code) + print(code_scope) + print() + exec(compile(code, '', 'exec'), code_scope) + self.set_data(input_func_name, code_scope[input_func_name]) + # results + self.formatted_funcs['input'] = { + 'func': code_scope[input_func_name], + 'scope': {host_name: self.host}, + 'call': [f'{host_name}.{input_func_name}(_i)'], + } + need_rebuild = True + return need_rebuild + + def get_monitor_func(self, mon_length, show_code=False): + mon = self.host.mon + if len(mon['vars']) > 0: + monitor_func_name = 'monitor_step' + host = self.host.name + code_scope = {host: self.host} + code_lines = [f'def {monitor_func_name}(_i):'] + for key in mon['vars']: + if not hasattr(self.host, key): + raise errors.ModelUseError(f'{self.host} do not have {key}, ' + f'thus it cannot be monitored.') + + # initialize monitor array # + shape = backend.shape(getattr(self.host, key)) + mon[key] = backend.zeros((mon_length,) + shape) + + # add line # + line = f' {host}.mon["{key}"][_i] = {host}.{key}' + code_lines.append(line) + + # function + code = '\n'.join(code_lines) + if show_code: + print(code) + print(code_scope) + print() + exec(compile(code, '', 'exec'), code_scope) + self.set_data(monitor_func_name, code_scope[monitor_func_name]) + # results + self.formatted_funcs['monitor'] = { + 'func': code_scope[monitor_func_name], + 'scope': {host: self.host}, + 'call': [f'{host}.{monitor_func_name}(_i)'], + } + + def get_steps_func(self, show_code=False): + for step in self.steps: + func_name = step.__name__ + class_args, arguments = utils.get_args(step) + host_name = self.host.name + + calls = [] + for arg in arguments: + if hasattr(self.host, arg): + calls.append(f'{host_name}.{arg}') + elif arg in backend.SYSTEM_KEYWORDS: + calls.append(arg) + else: + raise errors.ModelDefError(f'Step function "{func_name}" of {self.host} ' + f'define an unknown argument "{arg}" which is not ' + f'an attribute of {self.host} nor the system keywords ' + f'{backend.SYSTEM_KEYWORDS}.') + self.formatted_funcs[func_name] = { + 'func': step, + 'scope': {host_name: self.host}, + 'call': [f'{host_name}.{func_name}({", ".join(calls)})'] + } + + def set_data(self, key, data): + setattr(self.host, key, data) + + def build(self, formatted_inputs, mon_length, return_code=True, show_code=False): + # inputs check + # -- + assert isinstance(formatted_inputs, (tuple, list)) + need_rebuild = self.get_input_func(formatted_inputs, show_code=show_code) + self.formatted_funcs['need_rebuild'] = need_rebuild + + # the run function does not build before + # --- + if self.run_func is None: + # monitors + self.get_monitor_func(mon_length, show_code=show_code) + + # steps + self.get_steps_func(show_code=show_code) + + # reshape the monitor + self.host.mon.reshape(run_length=mon_length) + + # build the model + if need_rebuild or self.run_func is None: + code_scope = dict() + code_lines = ['def run_func(_t, _i, _dt):'] + for process in self.get_schedule(): + if (process not in self.formatted_funcs) and (process in ['input', 'monitor']): + continue + process_result = self.formatted_funcs[process] + code_scope.update(process_result['scope']) + code_lines.extend(process_result['call']) + + # function + code = '\n '.join(code_lines) + if show_code: + print(code) + print(code_scope) + print() + exec(compile(code, '', 'exec'), code_scope) + self.run_func = code_scope['run_func'] + + if return_code: + return self.run_func, self.formatted_funcs + else: + return self.run_func + + @staticmethod + def input_data_name(key): + return f'_input_data_of_{key.replace(".", "_")}' + + +class GeneralNetRunner(runner.NetRunner): + def __init__(self, all_nodes): + super(GeneralNetRunner, self).__init__(all_nodes=all_nodes) + self.run_func = None + + def build(self, run_length, formatted_inputs, return_code=False, show_code=False): + """Build the network. + + Parameters + ---------- + run_length : int + The running length. + formatted_inputs : dict + The user-defined inputs. + show_code : bool + Show the formatted code. + return_code : bool + Return the code lines and code scope. + + Returns + ------- + step_func : callable + The step function. + """ + if not isinstance(run_length, int): + raise errors.ModelUseError(f'The running length must be an int, ' + f'but we get {type(run_length)}') + + # codes for step function + need_rebuild = False + code_scope = {} + code_lines = ['def run_func(_t, _i, _dt):'] + for obj in self.all_nodes.values(): + f, codes = obj.build(formatted_inputs=formatted_inputs.get(obj.name, []), + mon_length=run_length, + return_code=True, + show_code=show_code) + need_rebuild *= codes['need_rebuild'] + for p in obj.get_schedule(): + p_codes = codes[p] + code_scope.update(p_codes['scope']) + code_lines.extend(p_codes['call']) + + # compile the step function + if (self.run_func is None) or need_rebuild: + code = '\n '.join(code_lines) + if show_code: + print(code) + print(code_scope) + print() + exec(compile(code, '', 'exec'), code_scope) + self.run_func = code_scope['run_func'] + + if return_code: + return self.run_func, code_lines, code_scope + else: + return self.run_func diff --git a/brainpy/backend/runners/jax_runner.py b/brainpy/backend/runners/jax_runner.py new file mode 100644 index 00000000..3511c7f6 --- /dev/null +++ b/brainpy/backend/runners/jax_runner.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + + +class JaxRunner(): + pass diff --git a/brainpy/backend/runners/numba_cpu_runner.py b/brainpy/backend/runners/numba_cpu_runner.py new file mode 100644 index 00000000..95d515e6 --- /dev/null +++ b/brainpy/backend/runners/numba_cpu_runner.py @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- + +import ast +import inspect +import re + +import numba + +from brainpy import backend +from brainpy import errors +from brainpy import profile +from brainpy import tools +from . import utils +from .general_runner import GeneralNodeRunner + +__all__ = [ + 'set_numba_profile', + 'get_numba_profile', + + 'StepFuncReader', + 'analyze_step_func', + 'get_func_body_code', + 'get_num_indent', + + 'NumbaCPUNodeRunner', +] + +NUMBA_PROFILE = { + 'nopython': True, + 'fastmath': True, + 'nogil': True, + 'parallel': False +} + + +def set_numba_profile(**kwargs): + """Set the compilation options of Numba JIT function. + + Parameters + ---------- + kwargs : Any + The arguments, including ``cache``, ``fastmath``, + ``parallel``, ``nopython``. + """ + global NUMBA_PROFILE + + if 'fastmath' in kwargs: + NUMBA_PROFILE['fastmath'] = kwargs.pop('fastmath') + if 'nopython' in kwargs: + NUMBA_PROFILE['nopython'] = kwargs.pop('nopython') + if 'nogil' in kwargs: + NUMBA_PROFILE['nogil'] = kwargs.pop('nogil') + if 'parallel' in kwargs: + NUMBA_PROFILE['parallel'] = kwargs.pop('parallel') + + +def get_numba_profile(): + """Get the compilation setting of numba JIT function. + + Returns + ------- + numba_setting : dict + Numba setting. + """ + return NUMBA_PROFILE + + +class StepFuncReader(ast.NodeVisitor): + def __init__(self): + self.lefts = [] + self.rights = [] + self.lines = [] + + def visit_Assign(self, node, level=0): + targets = [] + for target in node.targets: + targets.append(tools.ast2code(ast.fix_missing_locations(target))) + target = ' = '.join(targets) + self.lefts.append(target) + expr = tools.ast2code(ast.fix_missing_locations(node.value)) + self.rights.append(expr) + prefix = ' ' * level + self.lines.append(f'{prefix}{target} = {expr}') + return node + + def visit_AugAssign(self, node, level=0): + target = tools.ast2code(ast.fix_missing_locations(node.target)) + op = tools.ast2code(ast.fix_missing_locations(node.op)) + expr = tools.ast2code(ast.fix_missing_locations(node.value)) + prefix = ' ' * level + self.lefts.append(target) + self.rights.append(f"{target} {op} {expr}") + self.lines.append(f"{prefix}{target} {op}= {expr}") + return node + + def visit_AnnAssign(self, node): + raise NotImplementedError('Do not support an assignment with ' + 'a type annotation in Numba backend.') + + def visit_node_not_assign(self, node, level=0): + prefix = ' ' * level + expr = tools.ast2code(ast.fix_missing_locations(node)) + self.lines.append(f'{prefix}{expr}') + + def visit_Assert(self, node, level=0): + self.visit_node_not_assign(node, level) + + def visit_Expr(self, node, level=0): + self.visit_node_not_assign(node, level) + + def visit_Expression(self, node, level=0): + self.visit_node_not_assign(node, level) + + def visit_content_in_condition_control(self, node, level): + if isinstance(node, ast.Expr): + self.visit_Expr(node, level) + elif isinstance(node, ast.Assert): + self.visit_Assert(node, level) + elif isinstance(node, ast.Assign): + self.visit_Assign(node, level) + elif isinstance(node, ast.AugAssign): + self.visit_AugAssign(node, level) + elif isinstance(node, ast.If): + self.visit_If(node, level) + elif isinstance(node, ast.For): + self.visit_For(node, level) + elif isinstance(node, ast.While): + self.visit_While(node, level) + else: + code = tools.ast2code(ast.fix_missing_locations(node)) + raise errors.CodeError(f'BrainPy does not support {type(node)} ' + f'in Numba backend.\n\n{code}') + + def visit_If(self, node, level=0): + # If condition + prefix = ' ' * level + compare = tools.ast2code(ast.fix_missing_locations(node.test)) + self.lines.append(f'{prefix}if {compare}:') + # body + for expr in node.body: + self.visit_content_in_condition_control(expr, level + 1) + + # elif + while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If): + node = node.orelse[0] + compare = tools.ast2code(ast.fix_missing_locations(node.test)) + self.lines.append(f'{prefix}elif {compare}:') + for expr in node.body: + self.visit_content_in_condition_control(expr, level + 1) + + # else: + if len(node.orelse) > 0: + self.lines.append(f'{prefix}else:') + for expr in node.orelse: + self.visit_content_in_condition_control(expr, level + 1) + + def visit_For(self, node, level=0): + prefix = ' ' * level + # target + target = tools.ast2code(ast.fix_missing_locations(node.target)) + # iter + iter = tools.ast2code(ast.fix_missing_locations(node.iter)) + self.lefts.append(target) + self.rights.append(iter) + self.lines.append(prefix + f'for {target} in {iter}:') + # body + for expr in node.body: + self.visit_content_in_condition_control(expr, level + 1) + # else + if len(node.orelse) > 0: + self.lines.append(prefix + 'else:') + for expr in node.orelse: + self.visit_content_in_condition_control(expr, level + 1) + + def visit_While(self, node, level=0): + prefix = ' ' * level + # test + test = tools.ast2code(ast.fix_missing_locations(node.test)) + self.rights.append(test) + self.lines.append(prefix + f'while {test}:') + # body + for expr in node.body: + self.visit_content_in_condition_control(expr, level + 1) + # else + if len(node.orelse) > 0: + self.lines.append(prefix + 'else:') + for expr in node.orelse: + self.visit_content_in_condition_control(expr, level + 1) + + def visit_Try(self, node): + raise errors.CodeError('Do not support "try" handler in Numba backend.') + + def visit_With(self, node): + raise errors.CodeError('Do not support "with" block in Numba backend.') + + def visit_Raise(self, node): + raise errors.CodeError('Do not support "raise" statement in Numba backend.') + + def visit_Delete(self, node): + raise errors.CodeError('Do not support "del" operation in Numba backend.') + + +def analyze_step_func(f): + """Analyze the step functions in a population. + + Parameters + ---------- + f : callable + The step function. + + Returns + ------- + results : tuple + The code string of the function, the code scope, + the data need pass into the arguments, + the data need return. + """ + + code_string = tools.deindent(inspect.getsource(f)).strip() + tree = ast.parse(code_string) + + # arguments + # --- + args = tools.ast2code(ast.fix_missing_locations(tree.body[0].args)).split(',') + + # code lines + # --- + formatter = StepFuncReader() + formatter.visit(tree) + + # data assigned by self.xx in line right + # --- + self_data_in_right = [] + if args[0] in profile.CLASS_KEYWORDS: + code = ', \n'.join(formatter.rights) + self_data_in_right = re.findall('\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*\\b', code) + self_data_in_right = list(set(self_data_in_right)) + + # data assigned by self.xxx in line left + # --- + code = ', \n'.join(formatter.lefts) + self_data_without_index_in_left = [] + self_data_with_index_in_left = [] + if args[0] in profile.CLASS_KEYWORDS: + class_p1 = '\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*\\b' + self_data_without_index_in_left = set(re.findall(class_p1, code)) + class_p2 = '(\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*)\\[.*\\]' + self_data_with_index_in_left = set(re.findall(class_p2, code)) + self_data_without_index_in_left -= self_data_with_index_in_left + self_data_without_index_in_left = list(self_data_without_index_in_left) + + # code scope + # --- + closure_vars = inspect.getclosurevars(f) + code_scope = dict(closure_vars.nonlocals) + code_scope.update(closure_vars.globals) + + # final + # --- + self_data_in_right = sorted(self_data_in_right) + self_data_without_index_in_left = sorted(self_data_without_index_in_left) + self_data_with_index_in_left = sorted(self_data_with_index_in_left) + return code_string, code_scope, self_data_in_right, \ + self_data_without_index_in_left, self_data_with_index_in_left + + +def get_func_body_code(code_string, lambda_func=False): + """Get the main body code of a function. + + Parameters + ---------- + code_string : str + The code string of the function. + lambda_func : bool + Whether the code comes from a lambda function. + + Returns + ------- + code_body : str + The code body. + """ + if lambda_func: + splits = code_string.split(':') + if len(splits) != 2: + raise ValueError(f'Can not parse function: \n{code_string}') + main_code = f'return {splits[1]}' + else: + func_codes = code_string.split('\n') + idx = 0 + for i, line in enumerate(func_codes): + idx += 1 + line = line.replace(' ', '') + if '):' in line: + break + else: + raise ValueError(f'Can not parse function: \n{code_string}') + main_code = '\n'.join(func_codes[idx:]) + return main_code + + +def get_num_indent(code_string, spaces_per_tab=4): + """Get the indent of a patch of source code. + + Parameters + ---------- + code_string : str + The code string. + spaces_per_tab : int + The spaces per tab. + + Returns + ------- + num_indent : int + The number of the indent. + """ + lines = code_string.split('\n') + min_indent = 1000 + for line in lines: + line = line.replace('\t', ' ' * spaces_per_tab) + num_indent = len(line) - len(line.lstrip()) + if num_indent < min_indent: + min_indent = num_indent + return min_indent + + +class NumbaCPUNodeRunner(GeneralNodeRunner): + def get_steps_func(self, show_code=False): + for step in self.steps: + func_name = step.__name__ + class_arg, arguments = utils.get_args(step) + host_name = self.host.name + + # arguments 1 + calls = [] + for arg in arguments: + if hasattr(self.host, arg): + calls.append(f'{host_name}.{arg}') + elif arg in backend.SYSTEM_KEYWORDS: + calls.append(arg) + else: + raise errors.ModelDefError(f'Step function "{func_name}" of {self.host} ' + f'define an unknown argument "{arg}" which is not ' + f'an attribute of {self.host} nor the system keywords ' + f'{backend.SYSTEM_KEYWORDS}.') + + # analysis + code_string, code_scope, self_data_in_right, \ + self_data_without_index_in_left, self_data_with_index_in_left = analyze_step_func(step) + main_code = get_func_body_code(code_string) + num_indent = get_num_indent(main_code) + + # arguments 1: data need pass + data_need_pass = sorted(list(set(self_data_in_right + self_data_with_index_in_left))) + replaces = {} + new_args = arguments + [] + for data in data_need_pass: + splits = data.split('.') + if len(splits) == 2: + attr_name = splits[1] + attr_ = getattr(self.host, attr_name) + if callable(attr_): + replaces[data] = data.replace('.', '_') + code_scope[data.replace('.', '_')] = attr_ + continue + new_args.append(data.replace('.', '_')) + calls.append('.'.join([host_name] + splits[1:])) + replaces[data] = data.replace('.', '_') + + # data need return + assigns = [] + returns = [] + for data in self_data_without_index_in_left: + splits = data.split('.') + assigns.append('.'.join([host_name] + splits[1:])) + returns.append(data.replace('.', '_')) + replaces[data] = data.replace('.', '_') + + # code scope + code_scope[host_name] = self.host + + # codes + main_code = f'def new_{func_name}({", ".join(new_args)}):\n' + main_code + if len(returns): + main_code += f'\n{" " * num_indent}return {", ".join(returns)}' + main_code = tools.word_replace(main_code, replaces) + if show_code: + print(main_code) + print(code_scope) + print() + + # recompile + exec(compile(main_code, '', 'exec'), code_scope) + func = code_scope[f'new_{func_name}'] + func = numba.jit(**NUMBA_PROFILE)(func) + self.set_data(f'new_{func_name}', func) + + # finale + r_line = '' + if len(assigns): + r_line = f'{", ".join(assigns)} = ' + self.formatted_funcs[func_name] = { + 'func': func, + 'scope': {host_name: self.host, f'{host_name}_{func_name}': func}, + # 'call': [f'{r_line}{host_name}.new_{func_name}({", ".join(calls)})'] + 'call': [f'{r_line}{host_name}_{func_name}({", ".join(calls)})'] + } diff --git a/brainpy/backend/runners/numba_cuda_runner.py b/brainpy/backend/runners/numba_cuda_runner.py new file mode 100644 index 00000000..5d9cb8e6 --- /dev/null +++ b/brainpy/backend/runners/numba_cuda_runner.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from .numba_cpu_runner import NumbaCPUNodeRunner + +__all__ = [ + 'NumbaCudaNodeRunner', +] + + + +class NumbaCudaNodeRunner(NumbaCPUNodeRunner): + pass + diff --git a/brainpy/backend/runners/utils.py b/brainpy/backend/runners/utils.py new file mode 100644 index 00000000..f1214e86 --- /dev/null +++ b/brainpy/backend/runners/utils.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + + +import inspect + +from brainpy import errors +from brainpy import profile + +__all__ = [ + 'get_args' +] + + +def get_args(f): + """Get the function arguments. + + Parameters + ---------- + f : callable + The function. + + Returns + ------- + args : tuple + The variable names, the other arguments, and the original args. + """ + + # 1. get the function arguments + parameters = inspect.signature(f).parameters + + arguments = [] + for name, par in parameters.items(): + if par.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: + arguments.append(par.name) + + elif par.kind is inspect.Parameter.KEYWORD_ONLY: + arguments.append(par.name) + + elif par.kind is inspect.Parameter.VAR_POSITIONAL: + raise errors.ModelDefError('Step function do not support positional parameters, e.g., *args') + elif par.kind is inspect.Parameter.POSITIONAL_ONLY: + raise errors.ModelDefError('Step function do not support positional only parameters, e.g., /') + elif par.kind is inspect.Parameter.VAR_KEYWORD: + raise errors.ModelDefError(f'Step function do not support dict of keyword arguments: {str(par)}') + else: + raise errors.ModelDefError(f'Unknown argument type: {par.kind}') + + # 2. check the function arguments + class_kw = None + if arguments[0] in profile.CLASS_KEYWORDS: + class_kw = arguments[0] + arguments = arguments[1:] + for a in arguments: + if a in profile.CLASS_KEYWORDS: + raise errors.DiffEqError(f'Class keywords "{a}" must be defined ' + f'as the first argument.') + return class_kw, arguments diff --git a/brainpy/backend/utils.py b/brainpy/backend/utils.py deleted file mode 100644 index da809240..00000000 --- a/brainpy/backend/utils.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - - -import math - -import numba -import numpy - -from .. import profile - -__all__ = [ - 'func_in_numpy_or_math', - 'normal_like', -] - -# Get functions in math -_functions_in_math = [] -for key in dir(math): - if not key.startswith('__'): - _functions_in_math.append(getattr(math, key)) - -# Get functions in NumPy -_functions_in_numpy = [] -for key in dir(numpy): - if not key.startswith('__'): - _functions_in_numpy.append(getattr(numpy, key)) -for key in dir(numpy.random): - if not key.startswith('__'): - _functions_in_numpy.append(getattr(numpy.random, key)) -for key in dir(numpy.linalg): - if not key.startswith('__'): - _functions_in_numpy.append(getattr(numpy.linalg, key)) - - -def func_in_numpy_or_math(func): - return func in _functions_in_math or func in _functions_in_numpy - - -@numba.generated_jit(**profile.get_numba_profile()) -def normal_like(x): - if isinstance(x, (numba.types.Integer, numba.types.Float)): - return lambda x: numpy.random.normal() - else: - return lambda x: numpy.random.normal(0., 1.0, x.shape) diff --git a/brainpy/connectivity/base.py b/brainpy/connectivity/base.py index f0acf48a..375e5843 100644 --- a/brainpy/connectivity/base.py +++ b/brainpy/connectivity/base.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- -import numba as nb -import numpy as np +import abc -from .. import profile -from ..errors import ModelUseError +from brainpy import backend +from brainpy import errors + +try: + import numba as nb +except ModuleNotFoundError: + nb = None __all__ = [ - 'Connector', 'ij2mat', 'mat2ij', 'pre2post', @@ -16,9 +19,19 @@ __all__ = [ 'post2syn', 'pre_slice_syn', 'post_slice_syn', + + 'AbstractConnector', + 'Connector', ] +def _numba_backend(): + r = backend.get_backend().startswith('numba') + if r and nb is None: + raise errors.PackageMissingError('Please install numba for numba backend.') + return r + + def ij2mat(i, j, num_pre=None, num_post=None): """Convert i-j connection to matrix connection. @@ -39,17 +52,14 @@ def ij2mat(i, j, num_pre=None, num_post=None): A 2D ndarray connectivity matrix. """ if len(i) != len(j): - raise ModelUseError('"i" and "j" must be the equal length.') + raise errors.ModelUseError('"i" and "j" must be the equal length.') if num_pre is None: print('WARNING: "num_pre" is not provided, the result may not be accurate.') - num_pre = np.max(i) + num_pre = i.max() if num_post is None: print('WARNING: "num_post" is not provided, the result may not be accurate.') - num_post = np.max(j) - - i = np.asarray(i, dtype=np.int64) - j = np.asarray(j, dtype=np.int64) - conn_mat = np.zeros((num_pre, num_post), dtype=np.float_) + num_post = j.max() + conn_mat = backend.zeros((num_pre, num_post)) conn_mat[i, j] = 1. return conn_mat @@ -61,20 +71,18 @@ def mat2ij(conn_mat): ---------- conn_mat : np.ndarray Connectivity matrix with `(num_pre, num_post)` shape. - + Returns ------- conn_tuple : tuple (Pre-synaptic neuron indexes, post-synaptic neuron indexes). """ - conn_mat = np.asarray(conn_mat) - if np.ndim(conn_mat) != 2: - raise ModelUseError('Connectivity matrix must be in the shape of (num_pre, num_post).') - pre_ids, post_ids = np.where(conn_mat > 0) - pre_ids = np.ascontiguousarray(pre_ids, dtype=np.int_) - post_ids = np.ascontiguousarray(post_ids, dtype=np.int_) - return pre_ids, post_ids + if len(backend.shape(conn_mat)) != 2: + raise errors.ModelUseError('Connectivity matrix must be in the ' + 'shape of (num_pre, num_post).') + pre_ids, post_ids = backend.where(conn_mat > 0) + return backend.as_tensor(pre_ids), backend.as_tensor(post_ids) def pre2post(i, j, num_pre=None): @@ -95,20 +103,20 @@ def pre2post(i, j, num_pre=None): The conn list of pre2post. """ if len(i) != len(j): - raise ModelUseError('The length of "i" and "j" must be the same.') + raise errors.ModelUseError('The length of "i" and "j" must be the same.') if num_pre is None: print('WARNING: "num_pre" is not provided, the result may not be accurate.') - num_pre = np.max(i) + num_pre = i.max() pre2post_list = [[] for _ in range(num_pre)] for pre_id, post_id in zip(i, j): pre2post_list[pre_id].append(post_id) - pre2post_list = [np.array(l) for l in pre2post_list] + pre2post_list = [backend.as_tensor(l) for l in pre2post_list] - if profile.is_jit(): + if _numba_backend: pre2post_list_nb = nb.typed.List() for pre_id in range(num_pre): - pre2post_list_nb.append(np.int64(pre2post_list[pre_id])) + pre2post_list_nb.append(pre2post_list[pre_id]) pre2post_list = pre2post_list_nb return pre2post_list @@ -132,20 +140,20 @@ def post2pre(i, j, num_post=None): """ if len(i) != len(j): - raise ModelUseError('The length of "i" and "j" must be the same.') + raise errors.ModelUseError('The length of "i" and "j" must be the same.') if num_post is None: print('WARNING: "num_post" is not provided, the result may not be accurate.') - num_post = np.max(j) + num_post = j.max() post2pre_list = [[] for _ in range(num_post)] for pre_id, post_id in zip(i, j): post2pre_list[post_id].append(pre_id) - post2pre_list = [np.array(l) for l in post2pre_list] + post2pre_list = [backend.as_tensor(l) for l in post2pre_list] - if profile.is_jit(): + if _numba_backend(): post2pre_list_nb = nb.typed.List() for post_id in range(num_post): - post2pre_list_nb.append(np.int64(post2pre_list[post_id])) + post2pre_list_nb.append(post2pre_list[post_id]) post2pre_list = post2pre_list_nb return post2pre_list @@ -167,17 +175,17 @@ def pre2syn(i, num_pre=None): """ if num_pre is None: print('WARNING: "num_pre" is not provided, the result may not be accurate.') - num_pre = np.max(i) + num_pre = i.max() pre2syn_list = [[] for _ in range(num_pre)] for syn_id, pre_id in enumerate(i): pre2syn_list[pre_id].append(syn_id) - pre2syn_list = [np.array(l) for l in pre2syn_list] + pre2syn_list = [backend.as_tensor(l) for l in pre2syn_list] - if profile.is_jit(): + if _numba_backend(): pre2syn_list_nb = nb.typed.List() for pre_ids in pre2syn_list: - pre2syn_list_nb.append(np.int64(pre_ids)) + pre2syn_list_nb.append(pre_ids) pre2syn_list = pre2syn_list_nb return pre2syn_list @@ -200,17 +208,17 @@ def post2syn(j, num_post=None): """ if num_post is None: print('WARNING: "num_post" is not provided, the result may not be accurate.') - num_post = np.max(j) + num_post = j.max() post2syn_list = [[] for _ in range(num_post)] for syn_id, post_id in enumerate(j): post2syn_list[post_id].append(syn_id) - post2syn_list = [np.array(l) for l in post2syn_list] + post2syn_list = [backend.as_tensor(l) for l in post2syn_list] - if profile.is_jit(): + if _numba_backend(): post2syn_list_nb = nb.typed.List() for pre_ids in post2syn_list: - post2syn_list_nb.append(np.int64(pre_ids)) + post2syn_list_nb.append(pre_ids) post2syn_list = post2syn_list_nb return post2syn_list @@ -235,16 +243,21 @@ def pre_slice_syn(i, j, num_pre=None): """ # check if len(i) != len(j): - raise ModelUseError('The length of "i" and "j" must be the same.') + raise errors.ModelUseError('The length of "i" and "j" must be the same.') if num_pre is None: print('WARNING: "num_pre" is not provided, the result may not be accurate.') - num_pre = np.max(i) + num_pre = i.max() # pre2post connection pre2post_list = [[] for _ in range(num_pre)] for pre_id, post_id in zip(i, j): pre2post_list[pre_id].append(post_id) - post_ids = np.asarray(np.concatenate(pre2post_list), dtype=np.int_) + pre_ids, post_ids = [], [] + for pre_i, posts in enumerate(pre2post_list): + post_ids.extend(posts) + pre_ids.extend([pre_i] * len(posts)) + post_ids = backend.as_tensor(post_ids) + pre_ids = backend.as_tensor(pre_ids) # pre2post slicing slicing = [] @@ -253,10 +266,7 @@ def pre_slice_syn(i, j, num_pre=None): end = start + len(posts) slicing.append([start, end]) start = end - slicing = np.asarray(slicing, dtype=np.int_) - - # pre_ids - pre_ids = np.repeat(np.arange(num_pre), slicing[:, 1] - slicing[:, 0]) + slicing = backend.as_tensor(slicing) return pre_ids, post_ids, slicing @@ -279,16 +289,21 @@ def post_slice_syn(i, j, num_post=None): The conn list of post2syn. """ if len(i) != len(j): - raise ModelUseError('The length of "i" and "j" must be the same.') + raise errors.ModelUseError('The length of "i" and "j" must be the same.') if num_post is None: print('WARNING: "num_post" is not provided, the result may not be accurate.') - num_post = np.max(j) + num_post = j.max() # post2pre connection post2pre_list = [[] for _ in range(num_post)] for pre_id, post_id in zip(i, j): post2pre_list[post_id].append(pre_id) - pre_ids = np.asarray(np.concatenate(post2pre_list), dtype=np.int_) + pre_ids, post_ids = [], [] + for _post_id, _pre_ids in enumerate(post2pre_list): + pre_ids.extend(_pre_ids) + post_ids.extend([_post_id] * len(_pre_ids)) + post_ids = backend.as_tensor(post_ids) + pre_ids = backend.as_tensor(pre_ids) # post2pre slicing slicing = [] @@ -297,15 +312,17 @@ def post_slice_syn(i, j, num_post=None): end = start + len(pres) slicing.append([start, end]) start = end - slicing = np.asarray(slicing, dtype=np.int_) - - # post_ids - post_ids = np.repeat(np.arange(num_post), slicing[:, 1] - slicing[:, 0]) + slicing = backend.as_tensor(slicing) return pre_ids, post_ids, slicing -class Connector(object): +class AbstractConnector(abc.ABC): + def __call__(self, *args, **kwargs): + pass + + +class Connector(AbstractConnector): """Abstract connector class.""" def __init__(self): @@ -313,6 +330,7 @@ class Connector(object): # useful for the construction of pre2post/pre2syn/etc. self.num_pre = None self.num_post = None + # synaptic structures self.pre_ids = None self.post_ids = None @@ -323,26 +341,11 @@ class Connector(object): self.post2syn = None self.pre_slice_syn = None self.post_slice_syn = None + # synaptic weights self.weights = None - # the required synaptic structures - self.requires = () - - def set_size(self, num_pre, num_post): - try: - assert isinstance(num_pre, int) - assert 0 < num_pre - except AssertionError: - raise ModelUseError('"num_pre" must be integrator bigger than 0.') - try: - assert isinstance(num_post, int) - assert 0 < num_post - except AssertionError: - raise ModelUseError('"num_post" must be integrator bigger than 0.') - self.num_pre = num_pre - self.num_post = num_post - - def set_requires(self, syn_requires): + + def requires(self, syn_requires): # get synaptic requires requires = set() for n in syn_requires: @@ -351,19 +354,19 @@ class Connector(object): 'pre2syn', 'post2syn', 'pre_slice_syn', 'post_slice_syn']: requires.add(n) - self.requires = list(requires) + requires = list(requires) # synaptic structure to handle needs = [] - if 'pre_slice_syn' in self.requires and 'post_slice_syn' in self.requires: - raise ModelUseError('Cannot use "pre_slice_syn" and "post_slice_syn" simultaneously. \n' - 'We recommend you use "pre_slice_syn + post2syn" ' - 'or "post_slice_syn + pre2syn".') - elif 'pre_slice_syn' in self.requires: + if 'pre_slice_syn' in requires and 'post_slice_syn' in requires: + raise errors.ModelUseError('Cannot use "pre_slice_syn" and "post_slice_syn" ' + 'simultaneously. \nWe recommend you use "pre_slice_syn + ' + 'post2syn" or "post_slice_syn + pre2syn".') + elif 'pre_slice_syn' in requires: needs.append('pre_slice_syn') - elif 'post_slice_syn' in self.requires: + elif 'post_slice_syn' in requires: needs.append('post_slice_syn') - for n in self.requires: + for n in requires: if n in ['pre_slice_syn', 'post_slice_syn', 'pre_ids', 'post_ids']: continue needs.append(n) @@ -372,9 +375,6 @@ class Connector(object): for n in needs: getattr(self, f'make_{n}')() - def __call__(self, pre_indices, post_indices): - raise NotImplementedError - def make_conn_mat(self): if self.conn_mat is None: self.conn_mat = ij2mat(self.pre_ids, self.post_ids, self.num_pre, self.num_post) diff --git a/brainpy/connectivity/methods.py b/brainpy/connectivity/methods.py index 9ab42a7d..69f331eb 100644 --- a/brainpy/connectivity/methods.py +++ b/brainpy/connectivity/methods.py @@ -1,55 +1,232 @@ # -*- coding: utf-8 -*- -import numba as nb import numpy as np -from . import base -from .. import errors +from brainpy import backend +from brainpy import errors +from .base import Connector + +try: + import numba as nb +except ModuleNotFoundError: + nb = None + +__all__ = [ + 'One2One', 'one2one', + 'All2All', 'all2all', + 'GridFour', 'grid_four', + 'GridEight', 'grid_eight', + 'GridN', + 'FixedPostNum', + 'FixedPreNum', + 'FixedProb', + 'GaussianProb', + 'GaussianWeight', + 'DOG', + 'SmallWorld', + 'ScaleFree' +] + + +def _size2len(size): + if isinstance(size, int): + return size + elif isinstance(size, (tuple, list)): + a = 1 + for b in size: + a *= b + return a + else: + raise ValueError -if hasattr(nb.core, 'dispatcher'): - from numba.core.dispatcher import Dispatcher -else: - from numba.core import Dispatcher +def _grid_four(height, width, row, include_self): + conn_i = [] + conn_j = [] + + for col in range(width): + i_index = (row * width) + col + if 0 <= row - 1 < height: + j_index = ((row - 1) * width) + col + conn_i.append(i_index) + conn_j.append(j_index) + if 0 <= row + 1 < height: + j_index = ((row + 1) * width) + col + conn_i.append(i_index) + conn_j.append(j_index) + if 0 <= col - 1 < width: + j_index = (row * width) + col - 1 + conn_i.append(i_index) + conn_j.append(j_index) + if 0 <= col + 1 < width: + j_index = (row * width) + col + 1 + conn_i.append(i_index) + conn_j.append(j_index) + if include_self: + conn_i.append(i_index) + conn_j.append(i_index) + return conn_i, conn_j + + +def _grid_n(height, width, row, n, include_self): + conn_i = [] + conn_j = [] + for col in range(width): + i_index = (row * width) + col + for row_diff in range(-n, n + 1): + for col_diff in range(-n, n + 1): + if (not include_self) and (row_diff == col_diff == 0): + continue + if 0 <= row + row_diff < height and 0 <= col + col_diff < width: + j_index = ((row + row_diff) * width) + col + col_diff + conn_i.append(i_index) + conn_j.append(j_index) + return conn_i, conn_j + + +def _gaussian_weight(pre_i, pre_width, pre_height, + num_post, post_width, post_height, + w_max, w_min, sigma, normalize, include_self): + conn_i = [] + conn_j = [] + conn_w = [] + + # get normalized coordination + pre_coords = (pre_i // pre_width, pre_i % pre_width) + if normalize: + pre_coords = (pre_coords[0] / (pre_height - 1) if pre_height > 1 else 1., + pre_coords[1] / (pre_width - 1) if pre_width > 1 else 1.) + + for post_i in range(num_post): + if (pre_i == post_i) and (not include_self): + continue + + # get normalized coordination + post_coords = (post_i // post_width, post_i % post_width) + if normalize: + post_coords = (post_coords[0] / (post_height - 1) if post_height > 1 else 1., + post_coords[1] / (post_width - 1) if post_width > 1 else 1.) + + # Compute Euclidean distance between two coordinates + distance = (pre_coords[0] - post_coords[0]) ** 2 + distance += (pre_coords[1] - post_coords[1]) ** 2 + # get weight and conn + value = w_max * np.exp(-distance / (2.0 * sigma ** 2)) + if value > w_min: + conn_i.append(pre_i) + conn_j.append(post_i) + conn_w.append(value) + return conn_i, conn_j, conn_w -__all__ = ['One2One', 'one2one', - 'All2All', 'all2all', - 'GridFour', 'grid_four', - 'GridEight', 'grid_eight', - 'GridN', - 'FixedPostNum', 'FixedPreNum', 'FixedProb', - 'GaussianProb', 'GaussianWeight', 'DOG', - 'SmallWorld', 'ScaleFree'] +def _gaussian_prob(pre_i, pre_width, pre_height, + num_post, post_width, post_height, + p_min, sigma, normalize, include_self): + conn_i = [] + conn_j = [] + conn_p = [] + + # get normalized coordination + pre_coords = (pre_i // pre_width, pre_i % pre_width) + if normalize: + pre_coords = (pre_coords[0] / (pre_height - 1) if pre_height > 1 else 1., + pre_coords[1] / (pre_width - 1) if pre_width > 1 else 1.) + + for post_i in range(num_post): + if (pre_i == post_i) and (not include_self): + continue -class One2One(base.Connector): + # get normalized coordination + post_coords = (post_i // post_width, post_i % post_width) + if normalize: + post_coords = (post_coords[0] / (post_height - 1) if post_height > 1 else 1., + post_coords[1] / (post_width - 1) if post_width > 1 else 1.) + + # Compute Euclidean distance between two coordinates + distance = (pre_coords[0] - post_coords[0]) ** 2 + distance += (pre_coords[1] - post_coords[1]) ** 2 + # get weight and conn + value = np.exp(-distance / (2.0 * sigma ** 2)) + if value > p_min: + conn_i.append(pre_i) + conn_j.append(post_i) + conn_p.append(value) + return conn_i, conn_j, conn_p + + +def _dog(pre_i, pre_width, pre_height, + num_post, post_width, post_height, + w_max_p, w_max_n, w_min, sigma_p, sigma_n, + normalize, include_self): + conn_i = [] + conn_j = [] + conn_w = [] + + # get normalized coordination + pre_coords = (pre_i // pre_width, pre_i % pre_width) + if normalize: + pre_coords = (pre_coords[0] / (pre_height - 1) if pre_height > 1 else 1., + pre_coords[1] / (pre_width - 1) if pre_width > 1 else 1.) + + for post_i in range(num_post): + if (pre_i == post_i) and (not include_self): + continue + + # get normalized coordination + post_coords = (post_i // post_width, post_i % post_width) + if normalize: + post_coords = (post_coords[0] / (post_height - 1) if post_height > 1 else 1., + post_coords[1] / (post_width - 1) if post_width > 1 else 1.) + + # Compute Euclidean distance between two coordinates + distance = (pre_coords[0] - post_coords[0]) ** 2 + distance += (pre_coords[1] - post_coords[1]) ** 2 + # get weight and conn + value = w_max_p * np.exp(-distance / (2.0 * sigma_p ** 2)) - \ + w_max_n * np.exp(-distance / (2.0 * sigma_n ** 2)) + if np.abs(value) > w_min: + conn_i.append(pre_i) + conn_j.append(post_i) + conn_w.append(value) + return conn_i, conn_j, conn_w + + +if nb is not None: + _grid_four = nb.njit(_grid_four) + _grid_n = nb.njit(_grid_n) + _gaussian_weight = nb.njit(_gaussian_weight) + _gaussian_prob = nb.njit(_gaussian_prob) + _dog = nb.njit(_dog) + + +class One2One(Connector): """ Connect two neuron groups one by one. This means The two neuron groups should have the same size. """ + def __init__(self): super(One2One, self).__init__() - def __call__(self, pre_indices, post_indices): - pre_indices = np.asarray(pre_indices) - post_indices = np.asarray(post_indices) - self.pre_ids = np.ascontiguousarray(pre_indices.flatten(), dtype=np.int_) - self.post_ids = np.ascontiguousarray(post_indices.flatten(), dtype=np.int_) + def __call__(self, pre_size, post_size): try: - assert np.size(self.pre_ids) == np.size(self.post_ids) + assert pre_size == post_size except AssertionError: raise errors.ModelUseError(f'One2One connection must be defined in two groups with the same size, ' - f'but we got {np.size(self.pre_ids)} != {np.size(self.post_ids)}.') - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() + f'but we got {pre_size} != {post_size}.') + + length = _size2len(pre_size) + self.num_pre = length + self.num_post = length + + self.pre_ids = backend.arange(length) + self.post_ids = backend.arange(length) one2one = One2One() -class All2All(base.Connector): +class All2All(Connector): """Connect each neuron in first group to all neurons in the post-synaptic neuron groups. It means this kind of conn will create (num_pre x num_post) synapses. @@ -59,73 +236,45 @@ class All2All(base.Connector): self.include_self = include_self super(All2All, self).__init__() - def __call__(self, pre_indices, post_indices): - pre_indices = pre_indices.flatten() - post_indices = post_indices.flatten() - num_pre, num_post = len(pre_indices), len(post_indices) - mat = np.ones((num_pre, num_post)) + def __call__(self, pre_size, post_size): + pre_len = _size2len(pre_size) + post_len = _size2len(post_size) + self.num_pre = pre_len + self.num_post = post_len + + mat = np.ones((pre_len, post_len)) if not self.include_self: - for i in range(min([num_post, num_pre])): - mat[i, i] = 0 - pre_ids, post_ids = np.where(mat > 0) - self.pre_ids = np.ascontiguousarray(pre_ids, dtype=np.int_) - self.post_ids = np.ascontiguousarray(post_ids, dtype=np.int_) - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() + eye = np.arange(min([pre_len, post_len])) + self.conn_mat[eye, eye] = 0 + self.conn_mat = backend.as_tensor(mat) all2all = All2All(include_self=True) -@nb.njit -def _grid_four(height, width, row, include_self): - conn_i = [] - conn_j = [] - for col in range(width): - i_index = (row * width) + col - if 0 <= row - 1 < height: - j_index = ((row - 1) * width) + col - conn_i.append(i_index) - conn_j.append(j_index) - if 0 <= row + 1 < height: - j_index = ((row + 1) * width) + col - conn_i.append(i_index) - conn_j.append(j_index) - if 0 <= col - 1 < width: - j_index = (row * width) + col - 1 - conn_i.append(i_index) - conn_j.append(j_index) - if 0 <= col + 1 < width: - j_index = (row * width) + col + 1 - conn_i.append(i_index) - conn_j.append(j_index) - if include_self: - conn_i.append(i_index) - conn_j.append(i_index) - return conn_i, conn_j - - -class GridFour(base.Connector): +class GridFour(Connector): """The nearest four neighbors conn method.""" def __init__(self, include_self=False): super(GridFour, self).__init__() self.include_self = include_self - def __call__(self, pre_indices, post_indices=None): - if post_indices is not None: + def __call__(self, pre_size, post_size=None): + self.num_pre = _size2len(pre_size) + if post_size is not None: try: - assert np.shape(pre_indices) == np.shape(post_indices) + assert pre_size == post_size except AssertionError: - raise errors.ModelUseError(f'The shape of pre-synaptic group should be the same with the post group. ' - f'But we got {np.shape(pre_indices)} != {np.shape(post_indices)}.') + raise errors.ModelUseError(f'The shape of pre-synaptic group should be the same with the ' + f'post group. But we got {pre_size} != {post_size}.') + self.num_post = _size2len(post_size) + else: + self.num_post = self.num_pre - if len(pre_indices.shape) == 1: - height, width = pre_indices.shape[0], 1 - elif len(pre_indices.shape) == 2: - height, width = pre_indices.shape + if len(pre_size) == 1: + height, width = pre_size[0], 1 + elif len(pre_size) == 2: + height, width = pre_size else: raise errors.ModelUseError('Currently only support two-dimensional geometry.') conn_i = [] @@ -134,43 +283,14 @@ class GridFour(base.Connector): a = _grid_four(height, width, row, include_self=self.include_self) conn_i.extend(a[0]) conn_j.extend(a[1]) - conn_i = np.asarray(conn_i) - conn_j = np.asarray(conn_j) - - pre_indices = pre_indices.flatten() - self.pre_ids = pre_indices[conn_i] - if self.num_pre is None: - self.num_pre = pre_indices.max() - if post_indices is None: - self.post_ids = pre_indices[conn_j] - else: - post_indices = post_indices.flatten() - self.post_ids = post_indices[conn_j] - if self.num_post is None: - self.num_post = post_indices.max() + self.pre_ids = backend.as_tensor(conn_i) + self.post_ids = backend.as_tensor(conn_j) grid_four = GridFour() -@nb.njit -def _grid_n(height, width, row, n, include_self): - conn_i = [] - conn_j = [] - for col in range(width): - i_index = (row * width) + col - for row_diff in range(-n, n + 1): - for col_diff in range(-n, n + 1): - if (not include_self) and (row_diff == col_diff == 0): - continue - if 0 <= row + row_diff < height and 0 <= col + col_diff < width: - j_index = ((row + row_diff) * width) + col + col_diff - conn_i.append(i_index) - conn_j.append(j_index) - return conn_i, conn_j - - -class GridN(base.Connector): +class GridN(Connector): """The nearest (2*N+1) * (2*N+1) neighbors conn method. Parameters @@ -196,18 +316,23 @@ class GridN(base.Connector): self.n = n self.include_self = include_self - def __call__(self, pre_indices, post_indices=None): - if post_indices is not None: + def __call__(self, pre_size, post_size=None): + self.num_pre = _size2len(pre_size) + if post_size is not None: try: - assert np.shape(pre_indices) == np.shape(post_indices) + assert pre_size == post_size except AssertionError: - raise errors.ModelUseError(f'The shape of pre-synaptic group should be the same with the post group. ' - f'But we got {np.shape(pre_indices)} != {np.shape(post_indices)}.') + raise errors.ModelUseError( + f'The shape of pre-synaptic group should be the same with the post group. ' + f'But we got {pre_size} != {post_size}.') + self.num_post = _size2len(post_size) + else: + self.num_post = self.num_pre - if len(pre_indices.shape) == 1: - height, width = pre_indices.shape[0], 1 - elif len(pre_indices.shape) == 2: - height, width = pre_indices.shape + if len(pre_size) == 1: + height, width = pre_size[0], 1 + elif len(pre_size) == 2: + height, width = pre_size else: raise errors.ModelUseError('Currently only support two-dimensional geometry.') @@ -218,20 +343,8 @@ class GridN(base.Connector): n=self.n, include_self=self.include_self) conn_i.extend(res[0]) conn_j.extend(res[1]) - conn_i = np.asarray(conn_i, dtype=np.int_) - conn_j = np.asarray(conn_j, dtype=np.int_) - - pre_indices = pre_indices.flatten() - if self.num_pre is None: - self.num_pre = pre_indices.max() - self.pre_ids = pre_indices[conn_i] - if post_indices is None: - self.post_ids = pre_indices[conn_j] - else: - post_indices = post_indices.flatten() - self.post_ids = post_indices[conn_j] - if self.num_post is None: - self.num_post = post_indices.max() + self.pre_ids = backend.as_tensor(conn_i) + self.post_ids = backend.as_tensor(conn_j) class GridEight(GridN): @@ -244,7 +357,7 @@ class GridEight(GridN): grid_eight = GridEight() -class FixedProb(base.Connector): +class FixedProb(Connector): """Connect the post-synaptic neurons with fixed probability. Parameters @@ -263,27 +376,22 @@ class FixedProb(base.Connector): self.include_self = include_self self.seed = seed - def __call__(self, pre_indices, post_indices): - pre_indices = pre_indices.flatten() - post_indices = post_indices.flatten() + def __call__(self, pre_size, post_size): + num_pre, num_post = _size2len(pre_size), _size2len(post_size) + self.num_pre, self.num_post = num_pre, num_post - num_pre, num_post = len(pre_indices), len(post_indices) prob_mat = np.random.random(size=(num_pre, num_post)) if not self.include_self: diag_index = np.arange(min([num_pre, num_post])) prob_mat[diag_index, diag_index] = 1. - conn_mat = prob_mat < self.prob + conn_mat = np.array(prob_mat < self.prob, dtype=np.int_) pre_ids, post_ids = np.where(conn_mat) - self.conn_mat = np.float_(conn_mat) - self.pre_ids = pre_indices[pre_ids] - self.post_ids = post_indices[post_ids] - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() + self.conn_mat = backend.as_tensor(conn_mat) + self.pre_ids = backend.as_tensor(np.ascontiguousarray(pre_ids)) + self.post_ids = backend.as_tensor(np.ascontiguousarray(post_ids)) -class FixedPreNum(base.Connector): +class FixedPreNum(Connector): """Connect the pre-synaptic neurons with fixed number for each post-synaptic neuron. @@ -310,10 +418,9 @@ class FixedPreNum(base.Connector): self.include_self = include_self self.seed = seed - def __call__(self, pre_indices, post_indices): - pre_indices = pre_indices.flatten() - post_indices = post_indices.flatten() - num_pre, num_post = len(pre_indices), len(post_indices) + def __call__(self, pre_size, post_size): + num_pre, num_post = _size2len(pre_size), _size2len(post_size) + self.num_pre, self.num_post = num_pre, num_post num = self.num if isinstance(self.num, int) else int(self.num * num_pre) assert num <= num_pre, f'"num" must be less than "num_pre", but got {num} > {num_pre}' prob_mat = np.random.random(size=(num_pre, num_post)) @@ -321,15 +428,13 @@ class FixedPreNum(base.Connector): diag_index = np.arange(min([num_pre, num_post])) prob_mat[diag_index, diag_index] = 1.1 arg_sort = np.argsort(prob_mat, axis=0)[:num] - self.pre_ids = np.asarray(np.concatenate(arg_sort), dtype=np.int64) - self.post_ids = np.asarray(np.repeat(np.arange(num_post), num_pre), dtype=np.int64) - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() + pre_ids = np.asarray(np.concatenate(arg_sort), dtype=np.int_) + post_ids = np.asarray(np.repeat(np.arange(num_post), num_pre), dtype=np.int_) + self.pre_ids = backend.as_tensor(pre_ids) + self.post_ids = backend.as_tensor(post_ids) -class FixedPostNum(base.Connector): +class FixedPostNum(Connector): """Connect the post-synaptic neurons with fixed number for each pre-synaptic neuron. @@ -356,11 +461,11 @@ class FixedPostNum(base.Connector): self.seed = seed super(FixedPostNum, self).__init__() - def __call__(self, pre_indices, post_indices): - pre_indices = pre_indices.flatten() - post_indices = post_indices.flatten() - num_pre = len(pre_indices) - num_post = len(post_indices) + def __call__(self, pre_size, post_size): + num_pre = _size2len(pre_size) + num_post = _size2len(post_size) + self.num_pre = num_pre + self.num_post = num_post num = self.num if isinstance(self.num, int) else int(self.num * num_post) assert num <= num_post, f'"num" must be less than "num_post", but got {num} > {num_post}' prob_mat = np.random.random(size=(num_pre, num_post)) @@ -368,51 +473,13 @@ class FixedPostNum(base.Connector): diag_index = np.arange(min([num_pre, num_post])) prob_mat[diag_index, diag_index] = 1.1 arg_sort = np.argsort(prob_mat, axis=1)[:, num] - self.post_ids = np.asarray(np.concatenate(arg_sort), dtype=np.int64) - self.pre_ids = np.asarray(np.repeat(np.arange(num_pre), num_post), dtype=np.int64) - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() - - -@nb.njit -def _gaussian_weight(pre_i, pre_width, pre_height, - num_post, post_width, post_height, - w_max, w_min, sigma, normalize, include_self): - conn_i = [] - conn_j = [] - conn_w = [] - - # get normalized coordination - pre_coords = (pre_i // pre_width, pre_i % pre_width) - if normalize: - pre_coords = (pre_coords[0] / (pre_height - 1) if pre_height > 1 else 1., - pre_coords[1] / (pre_width - 1) if pre_width > 1 else 1.) - - for post_i in range(num_post): - if (pre_i == post_i) and (not include_self): - continue - - # get normalized coordination - post_coords = (post_i // post_width, post_i % post_width) - if normalize: - post_coords = (post_coords[0] / (post_height - 1) if post_height > 1 else 1., - post_coords[1] / (post_width - 1) if post_width > 1 else 1.) - - # Compute Euclidean distance between two coordinates - distance = (pre_coords[0] - post_coords[0]) ** 2 - distance += (pre_coords[1] - post_coords[1]) ** 2 - # get weight and conn - value = w_max * np.exp(-distance / (2.0 * sigma ** 2)) - if value > w_min: - conn_i.append(pre_i) - conn_j.append(post_i) - conn_w.append(value) - return conn_i, conn_j, conn_w + post_ids = np.asarray(np.concatenate(arg_sort), dtype=np.int64) + pre_ids = np.asarray(np.repeat(np.arange(num_pre), num_post), dtype=np.int64) + self.pre_ids = backend.as_tensor(pre_ids) + self.post_ids = backend.as_tensor(post_ids) -class GaussianWeight(base.Connector): +class GaussianWeight(Connector): """Builds a Gaussian conn pattern between the two populations, where the weights decay with gaussian function. @@ -451,13 +518,15 @@ class GaussianWeight(base.Connector): self.normalize = normalize self.include_self = include_self - def __call__(self, pre_indices, post_indices): - num_pre = np.size(pre_indices) - num_post = np.size(post_indices) - assert np.ndim(pre_indices) == 2 - assert np.ndim(post_indices) == 2 - pre_height, pre_width = pre_indices.shape - post_height, post_width = post_indices.shape + def __call__(self, pre_size, post_size): + num_pre = _size2len(pre_size) + num_post = _size2len(post_size) + self.num_pre = num_pre + self.num_post = num_post + assert len(pre_size) == 2 + assert len(post_size) == 2 + pre_height, pre_width = pre_size + post_height, post_width = post_size # get the connections and weights i, j, w = [], [], [] @@ -480,54 +549,12 @@ class GaussianWeight(base.Connector): pre_ids = np.asarray(i, dtype=np.int_) post_ids = np.asarray(j, dtype=np.int_) w = np.asarray(w, dtype=np.float_) - pre_indices = pre_indices.flatten() - post_indices = post_indices.flatten() - self.pre_ids = pre_indices[pre_ids] - self.post_ids = post_indices[post_ids] - self.weights = w - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() - - -@nb.njit -def _gaussian_prob(pre_i, pre_width, pre_height, - num_post, post_width, post_height, - p_min, sigma, normalize, include_self): - conn_i = [] - conn_j = [] - conn_p = [] - - # get normalized coordination - pre_coords = (pre_i // pre_width, pre_i % pre_width) - if normalize: - pre_coords = (pre_coords[0] / (pre_height - 1) if pre_height > 1 else 1., - pre_coords[1] / (pre_width - 1) if pre_width > 1 else 1.) + self.pre_ids = backend.as_tensor(pre_ids) + self.post_ids = backend.as_tensor(post_ids) + self.weights = backend.as_tensor(w) - for post_i in range(num_post): - if (pre_i == post_i) and (not include_self): - continue - # get normalized coordination - post_coords = (post_i // post_width, post_i % post_width) - if normalize: - post_coords = (post_coords[0] / (post_height - 1) if post_height > 1 else 1., - post_coords[1] / (post_width - 1) if post_width > 1 else 1.) - - # Compute Euclidean distance between two coordinates - distance = (pre_coords[0] - post_coords[0]) ** 2 - distance += (pre_coords[1] - post_coords[1]) ** 2 - # get weight and conn - value = np.exp(-distance / (2.0 * sigma ** 2)) - if value > p_min: - conn_i.append(pre_i) - conn_j.append(post_i) - conn_p.append(value) - return conn_i, conn_j, conn_p - - -class GaussianProb(base.Connector): +class GaussianProb(Connector): """Builds a Gaussian conn pattern between the two populations, where the conn probability decay according to the gaussian function. @@ -559,13 +586,13 @@ class GaussianProb(base.Connector): self.normalize = normalize self.include_self = include_self - def __call__(self, pre_indices, post_indices): - num_pre = np.size(pre_indices) - num_post = np.size(post_indices) - assert np.ndim(pre_indices) == 2 - assert np.ndim(post_indices) == 2 - pre_height, pre_width = pre_indices.shape - post_height, post_width = post_indices.shape + def __call__(self, pre_size, post_size): + self.num_pre = num_pre = _size2len(pre_size) + self.num_post = num_post = _size2len(post_size) + assert len(pre_size) == 2 + assert len(post_size) == 2 + pre_height, pre_width = pre_size + post_height, post_width = post_size # get the connections i, j, p = [], [], [] # conn_i, conn_j, probabilities @@ -587,55 +614,11 @@ class GaussianProb(base.Connector): selected_idxs = np.where(np.random.random(len(p)) < p)[0] i = np.asarray(i, dtype=np.int_)[selected_idxs] j = np.asarray(j, dtype=np.int_)[selected_idxs] - pre_indices = pre_indices.flatten() - post_indices = post_indices.flatten() - self.pre_ids = pre_indices[i] - self.post_ids = post_indices[j] - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() + self.pre_ids = backend.as_tensor(i) + self.post_ids = backend.as_tensor(j) -@nb.njit -def _dog(pre_i, pre_width, pre_height, - num_post, post_width, post_height, - w_max_p, w_max_n, w_min, sigma_p, sigma_n, - normalize, include_self): - conn_i = [] - conn_j = [] - conn_w = [] - - # get normalized coordination - pre_coords = (pre_i // pre_width, pre_i % pre_width) - if normalize: - pre_coords = (pre_coords[0] / (pre_height - 1) if pre_height > 1 else 1., - pre_coords[1] / (pre_width - 1) if pre_width > 1 else 1.) - - for post_i in range(num_post): - if (pre_i == post_i) and (not include_self): - continue - - # get normalized coordination - post_coords = (post_i // post_width, post_i % post_width) - if normalize: - post_coords = (post_coords[0] / (post_height - 1) if post_height > 1 else 1., - post_coords[1] / (post_width - 1) if post_width > 1 else 1.) - - # Compute Euclidean distance between two coordinates - distance = (pre_coords[0] - post_coords[0]) ** 2 - distance += (pre_coords[1] - post_coords[1]) ** 2 - # get weight and conn - value = w_max_p * np.exp(-distance / (2.0 * sigma_p ** 2)) - \ - w_max_n * np.exp(-distance / (2.0 * sigma_n ** 2)) - if np.abs(value) > w_min: - conn_i.append(pre_i) - conn_j.append(post_i) - conn_w.append(value) - return conn_i, conn_j, conn_w - - -class DOG(base.Connector): +class DOG(Connector): """Builds a Difference-Of-Gaussian (dog) conn pattern between the two populations. Mathematically, @@ -671,13 +654,13 @@ class DOG(base.Connector): self.normalize = normalize self.include_self = include_self - def __call__(self, pre_indices, post_indices): - num_pre = np.size(pre_indices) - num_post = np.size(post_indices) - assert np.ndim(pre_indices) == 2 - assert np.ndim(post_indices) == 2 - pre_height, pre_width = pre_indices.shape - post_height, post_width = post_indices.shape + def __call__(self, pre_size, post_size): + self.num_pre = num_pre = _size2len(pre_size) + self.num_post = num_post = _size2len(post_size) + assert len(pre_size) == 2 + assert len(post_size) == 2 + pre_height, pre_width = pre_size + post_height, post_width = post_size # get the connections and weights i, j, w = [], [], [] # conn_i, conn_j, weights @@ -703,22 +686,16 @@ class DOG(base.Connector): i = np.asarray(i, dtype=np.int_) j = np.asarray(j, dtype=np.int_) w = np.asarray(w, dtype=np.float_) - pre_indices = pre_indices.flatten() - post_indices = post_indices.flatten() - self.pre_ids = pre_indices[i] - self.post_ids = post_indices[j] - self.weights = w - if self.num_pre is None: - self.num_pre = pre_indices.max() - if self.num_post is None: - self.num_post = post_indices.max() - - -class ScaleFree(base.Connector): + self.pre_ids = backend.as_tensor(i) + self.post_ids = backend.as_tensor(j) + self.weights = backend.as_tensor(w) + + +class ScaleFree(Connector): def __init__(self): raise NotImplementedError -class SmallWorld(base.Connector): +class SmallWorld(Connector): def __init__(self): raise NotImplementedError diff --git a/brainpy/core/__init__.py b/brainpy/core/__init__.py deleted file mode 100644 index dfb7f5bc..00000000 --- a/brainpy/core/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - -from .base import * -from .types import * -from .runner import * -from .neurons import * -from .synapses import * -from .network import * diff --git a/brainpy/core/base.py b/brainpy/core/base.py deleted file mode 100644 index 16d05873..00000000 --- a/brainpy/core/base.py +++ /dev/null @@ -1,609 +0,0 @@ -# -*- coding: utf-8 -*- - -import inspect -import re -import time -from copy import deepcopy - -import numpy as np -from numba import cuda - -from . import constants -from . import runner -from . import types -from . import utils -from .. import errors -from .. import profile -from .. import tools - -__all__ = [ - 'ObjType', - 'Ensemble', - 'ParsUpdate', -] - - -class ObjType(object): - """The base type of neuron and synapse. - - Parameters - ---------- - name : str, optional - Model name. - """ - - def __init__(self, ST, name, steps, requires=None, mode='vector', hand_overs=None, ): - self.mode = mode - self.name = name - if not isinstance(ST, types.ObjState): - raise errors.ModelDefError('"ST" must be an instance of ObjState.') - self.ST = ST - - # requires - # --------- - if requires is None: - requires = dict() - if not isinstance(requires, dict): - raise errors.ModelDefError('"requires" only supports dict.') - self.requires = requires - for k, v in requires.items(): - if isinstance(v, type): - raise errors.ModelDefError(f'In "requires", you must instantiate ' - f'the type checker of "{k}". ' - f'Like "{v.__name__}()".') - if not isinstance(v, types.TypeChecker): - raise errors.ModelDefError(f'In "requires", each value must be a ' - f'{types.TypeChecker.__name__}, ' - f'but got "{type(v)}" for "{k}".') - - # steps - # ------ - self.steps = [] - self.step_names = [] - self.step_scopes = dict() - self.step_args = set() - step_vars = set() - if callable(steps): - steps = [steps] - elif isinstance(steps, (list, tuple)): - steps = list(steps) - else: - raise errors.ModelDefError('"steps" must be a callable, or a ' - 'list/tuple of callable functions.') - for func in steps: - if not callable(func): - raise errors.ModelDefError('"steps" must be a list/tuple of callable functions.') - - # function name - func_name = tools.get_func_name(func, replace=True) - self.step_names.append(func_name) - - # function arg - for arg in inspect.getfullargspec(func).args: - if arg in constants.ARG_KEYWORDS: - continue - self.step_args.add(arg) - - # function scope - scope = utils.get_func_scope(func, include_dispatcher=True) - for k, v in scope.items(): - if k in self.step_scopes: - if v != self.step_scopes[k]: - raise errors.ModelDefError( - f'Find scope variable {k} have different values in ' - f'{self.name}: {k} = {v} and {k} = {self.step_scopes[k]}.\n' - f'This maybe cause a grievous mistake in the future. ' - f'Please change!') - self.step_scopes[k] = v - - # function - self.steps.append(func) - - # set attribute - setattr(self, func_name, func) - - # get the STATE variables - step_vars.update(re.findall(r'ST\[[\'"](\w+)[\'"]\]', tools.get_main_code(func))) - - self.step_args = list(self.step_args) - - # variables - # ---------- - self.variables = ST._vars - for var in step_vars: - if var not in self.variables: - raise errors.ModelDefError(f'Variable "{var}" is used in {self.name}, ' - f'but not defined in "ST".') - - # integrators - # ----------- - self.integrators = [] - for step in self.steps: - self.integrators.extend(utils.find_integrators(step)) - self.integrators = list(set(self.integrators)) - - # delay keys - # ---------- - self._delay_keys = [] - - # hand overs - # --------------- - if hand_overs is not None: - if not isinstance(hand_overs, dict): - raise errors.ModelUseError('"hand_overs" must be a dict.') - else: - hand_overs = dict() - self.hand_overs = hand_overs - - def __str__(self): - return f'{self.name}' - - -class ParsUpdate(dict): - """Class for parameter updating. - - Structure of ``ParsUpdate`` - - - origins : original parameters - - num : number of the neurons - - updates : parameters to update - - heters : parameters to update, and they are heterogeneous - - model : the model which this ParsUpdate belongs to - - """ - - def __init__(self, all_pars, num, model): - assert isinstance(all_pars, dict) - assert isinstance(num, int) - - super(ParsUpdate, self).__init__(origins=all_pars, - num=num, - heters=dict(), - updates=dict(), - model=model) - - def __setitem__(self, key, value): - # check the existence of "key" - if key not in self.origins: - raise errors.ModelUseError(f'Parameter "{key}" may be not defined in ' - f'"{self.model.name}" variable scope.\n' - f'Or, "{key}" is used to compute an ' - f'intermediate variable, and is not ' - f'directly used by the step functions.') - - # check value size - val_size = np.size(value) - if val_size != 1: - if val_size != self.num: - raise errors.ModelUseError( - f'The size of parameter "{key}" is wrong, "{val_size}" != 1 ' - f'and "{val_size}" != {self.num}.') - if np.size(self.origins[key]) != val_size: # maybe default value is a heterogeneous value - self.heters[key] = value - - # update - if profile.run_on_cpu(): - self.updates[key] = value - else: - if isinstance(value, (int, float)): - self.updates[key] = value - elif value.__class__.__name__ == 'DeviceNDArray': - self.updates[key] = value - elif isinstance(value, np.ndarray): - self.updates[key] = cuda.to_device(value) - else: - raise ValueError(f'GPU mode cannot support {type(value)}.') - - def __getitem__(self, item): - if item in self.updates: - return self.updates[item] - elif item in self.origins: - return self.origins[item] - else: - super(ParsUpdate, self).__getitem__(item) - - def __dir__(self): - return str(self.all) - - def keys(self): - """All parameters can be updated. - - Returns - ------- - keys : list - List of parameter names. - """ - return self.origins.keys() - - def items(self): - """All parameters, including keys and values. - - Returns - ------- - items : iterable - The iterable parameter items. - """ - return self.all.items() - - def get(self, item): - """Get the parameter value by its key. - - Parameters - ---------- - item : str - Parameter name. - - Returns - ------- - value : any - Parameter value. - """ - return self.all.__getitem__(item) - - @property - def origins(self): - return super(ParsUpdate, self).__getitem__('origins') - - @property - def heters(self): - return super(ParsUpdate, self).__getitem__('heters') - - @property - def updates(self): - return super(ParsUpdate, self).__getitem__('updates') - - @property - def num(self): - return super(ParsUpdate, self).__getitem__('num') - - @property - def model(self): - return super(ParsUpdate, self).__getitem__('model') - - @property - def all(self): - origins = deepcopy(self.origins) - origins.update(self.updates) - return origins - - -class Ensemble(object): - """Base Ensemble class. - - Parameters - ---------- - name : str - Name of the (neurons/synapses) ensemble. - num : int - The number of the neurons/synapses. - model : ObjType - The (neuron/synapse) model. - monitors : list, tuple, None - Variables to monitor. - pars_update : dict, None - Parameters to update. - cls_type : str - Class type. - """ - - def __init__(self, name, num, model, monitors, pars_update, cls_type, satisfies=None, ): - # class type - # ----------- - if not cls_type in [constants.NEU_GROUP_TYPE, constants.SYN_CONN_TYPE]: - raise errors.ModelUseError(f'Only support "{constants.NEU_GROUP_TYPE}" ' - f'and "{constants.SYN_CONN_TYPE}".') - self._cls_type = cls_type - - # model - # ----- - self.model = model - - # name - # ---- - self.name = name - if not self.name.isidentifier(): - raise errors.ModelUseError( - f'"{self.name}" isn\'t a valid identifier according to Python ' - f'language definition. Please choose another name.') - - # num - # --- - self.num = num - - # parameters - # ---------- - self.pars = ParsUpdate(all_pars=model.step_scopes, num=num, model=model) - pars_update = dict() if pars_update is None else pars_update - if not isinstance(pars_update, dict): - raise errors.ModelUseError('"pars_update" must be a dict.') - for k, v in pars_update.items(): - self.pars[k] = v - - # monitors - # --------- - self.mon = tools.DictPlus() - self._mon_items = [] - if monitors is not None: - if isinstance(monitors, (list, tuple)): - for var in monitors: - if isinstance(var, str): - self._mon_items.append((var, None)) - self.mon[var] = np.empty((1, 1), dtype=np.float_) - elif isinstance(var, (tuple, list)): - self._mon_items.append((var[0], var[1])) - self.mon[var[0]] = np.empty((1, 1), dtype=np.float_) - else: - raise errors.ModelUseError(f'Unknown monitor item: {str(var)}') - elif isinstance(monitors, dict): - for k, v in monitors.items(): - self._mon_items.append((k, v)) - self.mon[k] = np.empty((1, 1), dtype=np.float_) - else: - raise errors.ModelUseError(f'Unknown monitors type: {type(monitors)}') - - # runner - # ------- - self.runner = runner.Runner(ensemble=self) - - # hand overs - # ---------- - # 1. attributes - # 2. functions - for attr_key, attr_val in model.hand_overs.items(): - setattr(self, attr_key, attr_val) - - # satisfies - # --------- - if satisfies is not None: - if not isinstance(satisfies, dict): - raise errors.ModelUseError('"satisfies" must be dict.') - for key, val in satisfies.items(): - setattr(self, key, val) - - def _is_state_attr(self, arg): - try: - attr = getattr(self, arg) - except AttributeError: - return False - if self._cls_type == constants.NEU_GROUP_TYPE: - return isinstance(attr, types.NeuState) - elif self._cls_type == constants.SYN_CONN_TYPE: - return isinstance(attr, types.SynState) - else: - raise ValueError - - def type_checking(self): - """Check the data type needed for step function. - """ - # 1. check ST and its type - if not hasattr(self, 'ST'): - raise errors.ModelUseError(f'"{self.name}" doesn\'t have "ST" attribute.') - try: - self.model.ST.check(self.ST) - except errors.TypeMismatchError: - raise errors.ModelUseError(f'"{self.name}.ST" doesn\'t satisfy TypeChecker "{str(self.model.ST)}".') - - # 2. check requires and its type - for key, type_checker in self.model.requires.items(): - if not hasattr(self, key): - raise errors.ModelUseError(f'"{self.name}" doesn\'t have "{key}" attribute.') - try: - type_checker.check(getattr(self, key)) - except errors.TypeMismatchError: - raise errors.ModelUseError(f'"{self.name}.{key}" doesn\'t satisfy TypeChecker "{str(type_checker)}".') - - # 3. check data (function arguments) needed - for i, func in enumerate(self.model.steps): - for arg in inspect.getfullargspec(func).args: - if not (arg in constants.ARG_KEYWORDS + ['self']) and not hasattr(self, arg): - raise errors.ModelUseError( - f'Function "{self.model.step_names[i]}" in "{self.model.name}" ' - f'requires "{arg}" as argument, but "{arg}" is not defined in "{self.name}".') - - def reshape_mon(self, run_length): - for key, val in self.mon.items(): - if key == 'ts': - continue - shape = val.shape - if run_length < shape[0]: - self.mon[key] = val[:run_length] - elif run_length > shape[0]: - append = np.zeros((run_length - shape[0],) + shape[1:]) - self.mon[key] = np.vstack([val, append]) - if profile.run_on_gpu(): - for key, val in self.mon.items(): - key_gpu = f'mon_{key}_cuda' - val_gpu = cuda.to_device(val) - setattr(self.runner, key_gpu, val_gpu) - self.runner.gpu_data[key_gpu] = val_gpu - - def build(self, inputs=None, mon_length=0): - """Build the object for running. - - Parameters - ---------- - inputs : list, tuple - The object inputs. - mon_length : int - The monitor length. - - Returns - ------- - calls : list, tuple - The code lines to call step functions. - """ - - # 1. prerequisite - # --------------- - if profile.run_on_gpu(): - if self.model.mode != constants.SCALAR_MODE: - raise errors.ModelUseError(f'GPU mode only support scalar-based mode. ' - f'But {self.model} is a {self.model.mode}-based model.') - self.type_checking() - - # 2. Code results - # --------------- - code_results = dict() - # inputs - if inputs: - r = self.runner.get_codes_of_input(inputs) - code_results.update(r) - # monitors - if len(self._mon_items): - mon, r = self.runner.get_codes_of_monitor(self._mon_items, run_length=mon_length) - code_results.update(r) - self.mon.clear() - self.mon.update(mon) - # steps - r = self.runner.get_codes_of_steps() - code_results.update(r) - - # 3. code calls - # ------------- - calls = self.runner.merge_codes(code_results) - if self._cls_type == constants.SYN_CONN_TYPE: - if self.delay_len > 1: - calls.append(f'{self.name}.ST._update_delay_indices()') - - return calls - - def run(self, duration, inputs=(), report=False, report_percent=0.1): - """The running function. - - Parameters - ---------- - duration : float, int, tuple, list - The running duration. - inputs : list, tuple - The model inputs with the format of ``[(key, value [operation])]``. - report : bool - Whether report the running progress. - report_percent : float - The percent of progress to report. - """ - - # times - # ------ - if isinstance(duration, (int, float)): - start, end = 0., duration - elif isinstance(duration, (tuple, list)): - assert len(duration) == 2, 'Only support duration setting with the format of "(start, end)".' - start, end = duration - else: - raise ValueError(f'Unknown duration type: {type(duration)}') - times = np.asarray(np.arange(start, end, profile.get_dt()), dtype=np.float_) - run_length = times.shape[0] - - # check inputs - # ------------- - if not isinstance(inputs, (tuple, list)): - raise errors.ModelUseError('"inputs" must be a tuple/list.') - if len(inputs) and not isinstance(inputs[0], (list, tuple)): - if isinstance(inputs[0], str): - inputs = [inputs] - else: - raise errors.ModelUseError('Unknown input structure, only support inputs ' - 'with format of "(key, value, [operation])".') - for inp in inputs: - if not 2 <= len(inp) <= 3: - raise errors.ModelUseError('For each target, you must specify "(key, value, [operation])".') - if len(inp) == 3 and inp[2] not in constants.INPUT_OPERATIONS: - raise errors.ModelUseError(f'Input operation only supports ' - f'"{list(constants.INPUT_OPERATIONS.keys())}", ' - f'not "{inp[2]}".') - - # format inputs - # ------------- - formatted_inputs = [] - for inp in inputs: - # key - if not isinstance(inp[0], str): - raise errors.ModelUseError('For each input, input[0] must be a string ' - 'to specify variable of the target.') - key = inp[0] - # value and data type - if isinstance(inp[1], (int, float)): - val = inp[1] - data_type = 'fix' - elif isinstance(inp[1], np.ndarray): - val = inp[1] - if val.shape[0] == run_length: - data_type = 'iter' - else: - data_type = 'fix' - else: - raise errors.ModelUseError('For each input, input[1] must be a ' - 'numerical value to specify input values.') - # operation - if len(inp) == 3: - ops = inp[2] - else: - ops = '+' - # input - format_inp = (key, val, ops, data_type) - formatted_inputs.append(format_inp) - - # get step function - # ------------------- - lines_of_call = self.build(inputs=formatted_inputs, mon_length=run_length) - code_lines = ['def step_func(_t, _i, _dt):'] - code_lines.extend(lines_of_call) - code_scopes = {self.name: self, f"{self.name}_runner": self.runner} - if profile.run_on_gpu(): - code_scopes['cuda'] = cuda - func_code = '\n '.join(code_lines) - exec(compile(func_code, '', 'exec'), code_scopes) - step_func = code_scopes['step_func'] - if profile.show_format_code(): - utils.show_code_str(func_code) - if profile.show_code_scope(): - utils.show_code_scope(code_scopes, ['__builtins__', 'step_func']) - - # run the model - # ------------- - dt = profile.get_dt() - if report: - t0 = time.time() - step_func(_t=times[0], _i=0, _dt=dt) - print('Compilation used {:.4f} s.'.format(time.time() - t0)) - - print("Start running ...") - report_gap = int(run_length * report_percent) - t0 = time.time() - for run_idx in range(1, run_length): - step_func(_t=times[run_idx], _i=run_idx, _dt=dt) - if (run_idx + 1) % report_gap == 0: - percent = (run_idx + 1) / run_length * 100 - print('Run {:.1f}% used {:.3f} s.'.format(percent, time.time() - t0)) - print('Simulation is done in {:.3f} s.'.format(time.time() - t0)) - else: - for run_idx in range(run_length): - step_func(_t=times[run_idx], _i=run_idx, _dt=dt) - - if profile.run_on_gpu(): - self.runner.gpu_data_to_cpu() - self.mon['ts'] = times - - def get_schedule(self): - """Get the schedule (running order) of the update functions. - - Returns - ------- - schedule : list, tuple - The running order of update functions. - """ - return self.runner.get_schedule() - - def set_schedule(self, schedule): - """Set the schedule (running order) of the update functions. - - For example, if the ``self.model`` has two step functions: `step1`, `step2`. - Then, you can set the shedule by using: - - >>> set_schedule(['input', 'step1', 'step2', 'monitor']) - """ - self.runner.set_schedule(schedule) - - @property - def requires(self): - return self.model.requires diff --git a/brainpy/core/constants.py b/brainpy/core/constants.py deleted file mode 100644 index 2d2f26f3..00000000 --- a/brainpy/core/constants.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -# argument keywords -KW_DT = '_dt' -KW_T = '_t' -KW_I = '_i' -ARG_KEYWORDS = ['_dt', '_t', '_i', '_obj_i', '_pre_i', '_post_i'] - -# name of the neuron group -NEU_GROUP_TYPE = 'NeuGroup' - -# name of the synapse connection -SYN_CONN_TYPE = 'SynConn' - -# input operations -INPUT_OPERATIONS = {'-': 'sub', - '+': 'add', - 'x': 'mul', - '*': 'mul', - '/': 'div', - '=': 'assign'} - -# model mode -SCALAR_MODE = 'scalar' -VECTOR_MODE = 'vector' -MATRIX_MODE = 'matrix' - - diff --git a/brainpy/core/network.py b/brainpy/core/network.py deleted file mode 100644 index 4e811f65..00000000 --- a/brainpy/core/network.py +++ /dev/null @@ -1,331 +0,0 @@ -# -*- coding: utf-8 -*- - -import time -from collections import OrderedDict - -import numpy as np -from numba import cuda - -from . import base -from . import constants -from . import utils -from .. import errors -from .. import profile - -__all__ = [ - 'Network', -] - - -class Network(object): - """The main simulation controller in ``BrainPy``. - - ``Network`` handles the running of a simulation. It contains a set of - objects that are added with `add()`. The `run()` method - actually runs the simulation. The main loop runs according to user add - orders. The objects in the `Network` are accessible via their names, e.g. - `net.name` would return the `object` (including neurons and synapses). - """ - - def __init__(self, *args, mode=None, **kwargs): - # record the current step - self.t_start = 0. - self.t_end = 0. - - # store all objects - self._all_objects = OrderedDict() - self.add(*args, **kwargs) - - # store the step function - self._step_func = None - - if isinstance(mode, str): - print('The "repeat" mode of the network is set to the default. ' - 'After version 0.4.0, "mode" setting will be removed.') - - def _add_obj(self, obj, name=None): - # 1. check object type - if not isinstance(obj, base.Ensemble): - raise ValueError(f'Unknown object type "{type(obj)}". Network ' - f'only supports NeuGroup and SynConn.') - # 2. check object name - name = obj.name if name is None else name - if name in self._all_objects: - raise KeyError(f'Name "{name}" has been used in the network, ' - f'please change another name.') - self._all_objects[name] = obj - # 3. add object to the network - setattr(self, name, obj) - if obj.name != name: - setattr(self, obj.name, obj) - - def add(self, *args, **kwargs): - """Add object (neurons or synapses) to the network. - - Parameters - ---------- - args - The nameless objects. - kwargs - The named objects, which can be accessed by `net.xxx` - (xxx is the name of the object). - """ - - for obj in args: - self._add_obj(obj) - for name, obj in kwargs.items(): - self._add_obj(obj, name) - - def format_inputs(self, inputs, run_length): - """Format the user defined inputs. - - Parameters - ---------- - inputs : tuple - The inputs. - run_length : int - The running length. - - Returns - ------- - formatted_input : dict - The formatted input. - """ - - # 1. format the inputs to standard - # formats and check the inputs - if not isinstance(inputs, (tuple, list)): - raise errors.ModelUseError('"inputs" must be a tuple/list.') - if len(inputs) > 0 and not isinstance(inputs[0], (list, tuple)): - if isinstance(inputs[0], base.Ensemble): - inputs = [inputs] - else: - raise errors.ModelUseError( - 'Unknown input structure. Only supports "(target, key, value, [operation])".') - for inp in inputs: - if not 3 <= len(inp) <= 4: - raise errors.ModelUseError('For each target, you must specify "(target, key, value, [operation])".') - if len(inp) == 4: - if inp[3] not in constants.INPUT_OPERATIONS: - raise errors.ModelUseError(f'Input operation only support ' - f'"{list(constants.INPUT_OPERATIONS.keys())}", ' - f'not "{inp[3]}".') - - # 2. format inputs - formatted_inputs = {} - for inp in inputs: - # target - if isinstance(inp[0], str): - target = getattr(self, inp[0]).name - elif isinstance(inp[0], base.Ensemble): - target = inp[0].name - else: - raise KeyError(f'Unknown input target: {str(inp[0])}') - - # key - if not isinstance(inp[1], str): - raise errors.ModelUseError('For each input, input[1] must be a string ' - 'to specify variable of the target.') - key = inp[1] - - # value and data type - if isinstance(inp[2], (int, float)): - val = inp[2] - data_type = 'fix' - elif isinstance(inp[2], np.ndarray): - val = inp[2] - if val.shape[0] == run_length: - data_type = 'iter' - else: - data_type = 'fix' - else: - raise errors.ModelUseError(f'For each input, input[2] must be a numerical value to ' - f'specify input values, but we get a {type(inp)}') - - # operation - if len(inp) == 4: - ops = inp[3] - else: - ops = '+' - - # final result - if target not in formatted_inputs: - formatted_inputs[target] = [] - format_inp = (key, val, ops, data_type) - formatted_inputs[target].append(format_inp) - return formatted_inputs - - def build(self, run_length, inputs=()): - """Build the network. - - Parameters - ---------- - run_length : int - The running length. - inputs : tuple, list - The user-defined inputs. - - Returns - ------- - step_func : callable - The step function. - """ - if not isinstance(run_length, int): - raise errors.ModelUseError(f'The running length must be an int, but we get {run_length}') - - # inputs - format_inputs = self.format_inputs(inputs, run_length) - - # codes for step function - code_scopes = {} - code_lines = ['# network step function\ndef step_func(_t, _i, _dt):'] - for obj in self._all_objects.values(): - if profile.run_on_gpu(): - if obj.model.mode != constants.SCALAR_MODE: - raise errors.ModelUseError(f'GPU mode only support scalar-based mode. ' - f'But {obj.model} is a {obj.model.mode}-based model.') - code_scopes[obj.name] = obj - code_scopes[f'{obj.name}_runner'] = obj.runner - lines_of_call = obj.build(inputs=format_inputs.get(obj.name, None), mon_length=run_length) - code_lines.extend(lines_of_call) - if profile.run_on_gpu(): - code_scopes['cuda'] = cuda - func_code = '\n '.join(code_lines) - - # compile the step function - exec(compile(func_code, '', 'exec'), code_scopes) - step_func = code_scopes['step_func'] - - # show - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def network_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scopes, ['__builtins__', 'step_func']) - - return step_func - - def run(self, duration, inputs=(), report=False, report_percent=0.1, - data_to_host=False, verbose=True): - """Run the simulation for the given duration. - - This function provides the most convenient way to run the network. - For example: - - Parameters - ---------- - duration : int, float, tuple, list - The amount of simulation time to run for. - inputs : list, tuple - The receivers, external inputs and durations. - report : bool - Report the progress of the simulation. - report_percent : float - The speed to report simulation progress. - data_to_host : bool - Transfer the gpu data to cpu. Available in CUDA backend. - verbose : bool - Show the error information. - """ - # check the duration - # ------------------ - if isinstance(duration, (int, float)): - start, end = 0, duration - elif isinstance(duration, (tuple, list)): - if len(duration) != 2: - raise errors.ModelUseError('Only support duration with the format of "(start, end)".') - start, end = duration - else: - raise ValueError(f'Unknown duration type: {type(duration)}') - self.t_start, self.t_end = start, end - dt = profile.get_dt() - ts = np.asarray(np.arange(start, end, dt), dtype=np.float_) - run_length = ts.shape[0] - - if self._step_func is None: - # initialize the function - # ----------------------- - self._step_func = self.build(run_length, inputs) - else: - # check and reset inputs - # ---------------------- - input_keep_same = True - formatted_inputs = self.format_inputs(inputs, run_length) - for obj in self._all_objects.values(): - obj_name = obj.name - obj_inputs = obj.runner._inputs - onj_input_keys = list(obj_inputs.keys()) - if obj_name in formatted_inputs: - current_inputs = formatted_inputs[obj_name] - else: - current_inputs = [] - for key, val, ops, data_type in current_inputs: - if np.shape(obj_inputs[key][0]) != np.shape(val): - if verbose: - print(f'The current "{key}" input shape {np.shape(val)} is different ' - f'from the last input shape {np.shape(obj_inputs[key][0])}. ') - input_keep_same = False - if obj_inputs[key][1] != ops: - if verbose: - print(f'The current "{key}" input operation "{ops}" is different ' - f'from the last operation "{obj_inputs[key][1]}". ') - input_keep_same = False - obj.runner.set_data(f'{key.replace(".", "_")}_inp', val) - if key in onj_input_keys: - onj_input_keys.remove(key) - else: - input_keep_same = False - if verbose: - print(f'The input to a new key "{key}" in {obj_name}.') - if len(onj_input_keys): - input_keep_same = False - if verbose: - print(f'The inputs of {onj_input_keys} in {obj_name} are not provided.') - if input_keep_same: - # reset monitors - # -------------- - for obj in self._all_objects.values(): - obj.reshape_mon(run_length) - else: - if verbose: - print('The network will be rebuild.') - self._step_func = self.build(run_length, inputs) - - dt = self.dt - if report: - # Run the model with progress report - # ---------------------------------- - t0 = time.time() - self._step_func(_t=ts[0], _i=0, _dt=dt) - print('Compilation used {:.4f} s.'.format(time.time() - t0)) - - print("Start running ...") - report_gap = int(run_length * report_percent) - t0 = time.time() - for run_idx in range(1, run_length): - self._step_func(_t=ts[run_idx], _i=run_idx, _dt=dt) - if (run_idx + 1) % report_gap == 0: - percent = (run_idx + 1) / run_length * 100 - print('Run {:.1f}% used {:.3f} s.'.format(percent, time.time() - t0)) - print('Simulation is done in {:.3f} s.'.format(time.time() - t0)) - else: - # Run the model - # ------------- - for run_idx in range(run_length): - self._step_func(_t=ts[run_idx], _i=run_idx, _dt=dt) - - # format monitor - # -------------- - for obj in self._all_objects.values(): - obj.mon['ts'] = self.ts - if data_to_host and profile.run_on_gpu(): - obj.runner.gpu_data_to_cpu() - - @property - def ts(self): - """Get the time points of the network. - """ - return np.array(np.arange(self.t_start, self.t_end, self.dt), dtype=np.float_) - - @property - def dt(self): - return profile.get_dt() diff --git a/brainpy/core/neurons.py b/brainpy/core/neurons.py deleted file mode 100644 index ef959c15..00000000 --- a/brainpy/core/neurons.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np - -from . import base -from . import constants -from . import utils -from .. import errors - -__all__ = [ - 'NeuType', - 'NeuGroup', - 'NeuSubGroup', -] - -_NEU_GROUP_NO = 0 - - -class NeuType(base.ObjType): - """Abstract Neuron Type. - - It can be defined based on a group of neurons or a single neuron. - """ - - def __init__(self, name, ST, steps, mode='vector', requires=None, hand_overs=None, ): - if mode not in [constants.SCALAR_MODE, constants.VECTOR_MODE]: - raise errors.ModelDefError('NeuType only support "scalar" or "vector".') - - super(NeuType, self).__init__(ST=ST, - requires=requires, - steps=steps, - name=name, - mode=mode, - hand_overs=hand_overs) - - -class NeuGroup(base.Ensemble): - """Neuron Group. - - Parameters - ---------- - model : NeuType - The instantiated neuron type model. - geometry : int, tuple - The neuron group geometry. - pars_update : dict - Parameters to update. - monitors : list, tuple - Variables to monitor. - name : str - The name of the neuron group. - """ - - def __init__(self, model, geometry, monitors=None, name=None, satisfies=None, pars_update=None, ): - # name - # ----- - if name is None: - global _NEU_GROUP_NO - name = f'NeuGroup{_NEU_GROUP_NO}' - _NEU_GROUP_NO += 1 - else: - name = name - - # num and geometry - # ----------------- - if isinstance(geometry, (int, float)): - geometry = num = int(geometry) - self.indices = np.asarray(np.arange(int(geometry)), dtype=np.int_) - elif isinstance(geometry, (tuple, list)): - if len(geometry) == 1: - geometry = num = geometry[0] - indices = np.arange(num) - elif len(geometry) == 2: - height, width = geometry[0], geometry[1] - num = height * width - indices = np.arange(num).reshape((height, width)) - else: - raise errors.ModelUseError('Do not support 3+ dimensional networks.') - self.indices = np.asarray(indices, dtype=np.int_) - else: - raise ValueError() - self.geometry = geometry - self.size = np.size(self.indices) - - # model - # ------ - try: - assert isinstance(model, NeuType) - except AssertionError: - raise errors.ModelUseError(f'{NeuGroup.__name__} receives an ' - f'instance of {NeuType.__name__}, ' - f'not {type(model).__name__}.') - - # initialize - # ---------- - super(NeuGroup, self).__init__(model=model, - pars_update=pars_update, - name=name, - num=num, - monitors=monitors, - cls_type=constants.NEU_GROUP_TYPE, - satisfies=satisfies) - - # ST - # -- - self.ST = self.model.ST.make_copy(num) - - def __getitem__(self, item): - """Return a subset of neuron group. - - Parameters - ---------- - item : slice, int, tuple of slice - - Returns - ------- - sub_group : NeuSubGroup - The subset of the neuron group. - """ - - if isinstance(item, int): - try: - assert item < self.num - except AssertionError: - raise errors.ModelUseError(f'Index error, because the maximum number of neurons' - f'is {self.num}, but got "item={item}".') - d1_start, d1_end, d1_step = item, item + 1, 1 - utils.check_slice(d1_start, d1_end, self.num) - indices = self.indices[d1_start:d1_end:d1_step] - elif isinstance(item, slice): - d1_start, d1_end, d1_step = item.indices(self.num) - utils.check_slice(d1_start, d1_end, self.num) - indices = self.indices[d1_start:d1_end:d1_step] - elif isinstance(item, tuple): - if not isinstance(self.geometry, (tuple, list)): - raise errors.ModelUseError(f'{self.name} has a 1D geometry, cannot use a tuple of slice.') - if len(item) != 2: - raise errors.ModelUseError(f'Only support 2D network, cannot make {len(item)}D slice.') - - if isinstance(item[0], slice): - d1_start, d1_end, d1_step = item[0].indices(self.geometry[0]) - elif isinstance(item[0], int): - d1_start, d1_end, d1_step = item[0], item[0] + 1, 1 - else: - raise errors.ModelUseError("Only support slicing syntax or a single index.") - utils.check_slice(d1_start, d1_end, self.geometry[0]) - - if isinstance(item[1], slice): - d2_start, d2_end, d2_step = item[1].indices(self.geometry[1]) - elif isinstance(item[1], int): - d2_start, d2_end, d2_step = item[1], item[1] + 1, 1 - else: - raise errors.ModelUseError("Only support slicing syntax or a single index.") - utils.check_slice(d1_start, d1_end, self.geometry[1]) - - indices = self.indices[d1_start:d1_end:d1_step, d2_start:d2_end:d2_step] - else: - raise errors.ModelUseError('Subgroups can only be constructed using slicing syntax, ' - 'a single index, or an array of contiguous indices.') - - return NeuSubGroup(source=self, indices=indices) - - -class NeuSubGroup(object): - """Subset of a `NeuGroup`. - """ - - def __init__(self, source, indices): - if not isinstance(source, NeuGroup): - raise errors.ModelUseError('NeuSubGroup only support an instance of NeuGroup.') - - self.source = source - self.indices = indices - self.num = np.size(indices) - - def __getattr__(self, item): - if item in ['source', 'indices', 'num']: - return getattr(self, item) - else: - return getattr(self.source, item) diff --git a/brainpy/core/runner.py b/brainpy/core/runner.py deleted file mode 100644 index 51f61b42..00000000 --- a/brainpy/core/runner.py +++ /dev/null @@ -1,1255 +0,0 @@ -# -*- coding: utf-8 -*- - -import ast -import inspect -import math -import re - -import numba -import numpy as np -from numba import cuda -from numba.cuda.random import create_xoroshiro128p_states -from numba.cuda.random import xoroshiro128p_normal_float64 - -from . import constants -from . import types -from . import utils -from .. import errors -from .. import integration -from .. import profile -from .. import tools -from ..tools import NoiseHandler - -__all__ = [ - 'Runner', - 'TrajectoryRunner', -] - - - -class Runner(object): - """Basic runner class. - - Parameters - ---------- - ensemble : NeuGroup, SynConn - The ensemble of the models. - """ - - def __init__(self, ensemble): - # ensemble: NeuGroup / SynConn - self.ensemble = ensemble - # ensemble model - self._model = ensemble.model - # ensemble name - self._name = ensemble.name - # ensemble parameters - self._pars = ensemble.pars - # model delay keys - self._delay_keys = ensemble.model._delay_keys - # model step functions - self._steps = ensemble.model.steps - self._step_names = ensemble.model.step_names - # model update schedule - self._schedule = ['input'] + ensemble.model.step_names + ['monitor'] - self._inputs = {} - self.gpu_data = {} - - def check_attr(self, attr): - if not hasattr(self, attr): - raise errors.ModelUseError(f'Model "{self._name}" doesn\'t have "{attr}" attribute", ' - f'and "{self._name}.ST" doesn\'t have "{attr}" field.') - - def get_codes_of_input(self, key_val_ops_types): - """Format the code of external input. - - Parameters - ---------- - key_val_ops_types : list, tuple - The inputs. - - Returns - ------- - code : dict - The formatted code. - """ - if len(key_val_ops_types) <= 0: - raise errors.ModelUseError(f'{self._name} has no input, cannot call this function.') - - # check datatype of the input - # ---------------------------- - has_iter = False - all_inputs = set() - for key, val, ops, t in key_val_ops_types: - if t not in ['iter', 'fix']: - raise errors.ModelUseError('Only support inputs of "iter" and "fix" types.') - if t == 'iter': - has_iter = True - if key in all_inputs: - raise errors.ModelUseError('Only support assignment for each key once.') - else: - self._inputs[key] = (val, ops, t) - all_inputs.add(key) - - # check data operations - # ---------------------- - for _, _, ops, _ in key_val_ops_types: - if ops not in constants.INPUT_OPERATIONS: - raise errors.ModelUseError( - f'Only support five input operations: {list(constants.INPUT_OPERATIONS.keys())}') - - # generate code of input function - # -------------------------------- - if profile.run_on_cpu(): - code_scope = {self._name: self.ensemble, f'{self._name}_runner': self} - code_args, code_arg2call, code_lines = set(), {}, [] - if has_iter: - code_args.add('_i') - code_arg2call['_i'] = '_i' - - input_idx = 0 - for key, val, ops, data_type in key_val_ops_types: - # get the left side # - attr_item = key.split('.') - if len(attr_item) == 1 and (attr_item[0] not in self.ensemble.ST): - # if "item" is the model attribute - attr, item = attr_item[0], '' - target = getattr(self.ensemble, attr) - self.check_attr(attr) - if not isinstance(target, np.ndarray): - raise errors.ModelUseError(f'BrainPy only support input to arrays.') - left = attr - code_args.add(left) - code_arg2call[left] = f'{self._name}.{attr}' - else: - if len(attr_item) == 1: - attr, item = 'ST', attr_item[0] - elif len(attr_item) == 2: - attr, item = attr_item[0], attr_item[1] - else: - raise errors.ModelUseError(f'Unknown target : {key}.') - data = getattr(self.ensemble, attr) - if item not in data: - raise errors.ModelUseError(f'"{self._name}.{attr}" doesn\'t have "{item}" field.') - idx = data['_var2idx'][item] - left = f'{attr}[{idx}]' - code_args.add(attr) - code_arg2call[attr] = f'{self._name}.{attr}["_data"]' - - # get the right side # - right = f'{key.replace(".", "_")}_inp' - code_args.add(right) - code_arg2call[right] = f'{self._name}_runner.{right}' - self.set_data(right, val) - if data_type == 'iter': - right = right + '[_i]' - if np.ndim(val) > 1: - pass - input_idx += 1 - - # final code line # - if ops == '=': - code_lines.append(f"{left} = {right}") - else: - code_lines.append(f"{left} {ops}= {right}") - - # final code - # ---------- - code_lines.insert(0, f'# "input" step function of {self._name}') - code_lines.append('\n') - - # compile function - code_to_compile = [f'def input_step({tools.func_call(code_args)}):'] + code_lines - func_code = '\n '.join(code_to_compile) - exec(compile(func_code, '', 'exec'), code_scope) - input_step = code_scope['input_step'] - # if profile.is_jit(): - # input_step = tools.jit(input_step) - self.input_step = input_step - if not profile.is_merge_steps(): - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def {self._name}_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scope, ['__builtins__', 'input_step']) - - # format function call - arg2call = [code_arg2call[arg] for arg in sorted(list(code_args))] - func_call = f'{self._name}_runner.input_step({tools.func_call(arg2call)})' - - return {'input': {'scopes': code_scope, - 'args': code_args, - 'arg2calls': code_arg2call, - 'codes': code_lines, - 'call': func_call}} - - else: - input_idx = 0 - results = {} - for key, val, ops, data_type in key_val_ops_types: - code_scope = {self._name: self.ensemble, f'{self._name}_runner': self, 'cuda': cuda} - code_args, code_arg2call, code_lines = set(), {}, [] - if has_iter: - code_args.add('_i') - code_arg2call['_i'] = '_i' - - attr_item = key.split('.') - if len(attr_item) == 1 and (attr_item[0] not in self.ensemble.ST): - # if "item" is the model attribute - attr, item = attr_item[0], '' - self.check_attr(attr) - target = getattr(self.ensemble, attr) - if not isinstance(target, np.ndarray): - raise errors.ModelUseError(f'BrainPy only supports input to arrays.') - # get the left side - left = f'{attr}[cuda_i]' - self.set_gpu_data(f'{attr}_cuda', target) - else: - # if "item" is the ObjState - if len(attr_item) == 1: - attr, item = 'ST', attr_item[0] - elif len(attr_item) == 2: - attr, item = attr_item[0], attr_item[1] - else: - raise errors.ModelUseError(f'Unknown target : {key}.') - data = getattr(self.ensemble, attr) - if item not in data: - raise errors.ModelUseError(f'"{self._name}.{attr}" doesn\'t have "{item}" field.') - # get the left side - target = data[item] - idx = data['_var2idx'][item] - left = f'{attr}[{idx}, cuda_i]' - self.set_gpu_data(f'{attr}_cuda', data) - code_args.add(f'{attr}') - code_arg2call[f'{attr}'] = f'{self._name}_runner.{attr}_cuda' - - # get the right side # - right = f'{key.replace(".", "_")}_inp' - self.set_data(right, val) - code_args.add(right) - code_arg2call[right] = f'{self._name}_runner.{right}' - - # check data type - iter_along_time = data_type == 'iter' - if np.isscalar(val): - iter_along_data = False - else: - if iter_along_time: - if np.isscalar(val[0]): - iter_along_data = False - else: - assert len(val[0]) == len(target) - iter_along_data = True - else: - assert len(val) == len(target) - iter_along_data = True - if iter_along_time and iter_along_data: - right = right + '[_i, cuda_i]' - elif iter_along_time: - right = right + '[_i]' - elif iter_along_data: - right = right + '[cuda_i]' - else: - right = right - - # final code line - if ops == '=': - code_lines.append(f"{left} = {right}") - else: - code_lines.append(f"{left} {ops}= {right}") - code_lines = [' ' + line for line in code_lines] - code_lines.insert(0, f'if cuda_i < {len(target)}:') - - # final code - func_name = f'input_of_{attr}_{item}' - code_to_compile = [f'# "input" of {self._name}.{attr}.{item}', - f'def {func_name}({tools.func_call(code_args)}):', - f' cuda_i = cuda.grid(1)'] - code_to_compile += [f' {line}' for line in code_lines] - - # compile function - func_code = '\n'.join(code_to_compile) - exec(compile(func_code, '', 'exec'), code_scope) - step_func = code_scope[func_name] - step_func = cuda.jit(step_func) - setattr(self, func_name, step_func) - if not profile.is_merge_steps(): - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def {self._name}_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scope, ['__builtins__', 'input_step']) - - # format function call - if len(target) <= profile.get_num_thread_gpu(): - num_thread = len(target) - num_block = 1 - else: - num_thread = profile.get_num_thread_gpu() - num_block = math.ceil(len(target) / profile.get_num_thread_gpu()) - arg2call = [code_arg2call[arg] for arg in sorted(list(code_args))] - func_call = f'{self._name}_runner.{func_name}[{num_block}, {num_thread}]({tools.func_call(arg2call)})' - - # function result - results[f'input-{input_idx}'] = {'scopes': code_scope, - 'args': code_args, - 'arg2calls': code_arg2call, - 'codes': code_lines, - 'call': func_call, - 'num_data': len(target)} - - # iteration - input_idx += 1 - - return results - - def get_codes_of_monitor(self, mon_vars, run_length): - """Get the code of the monitors. - - Parameters - ---------- - mon_vars : tuple, list - The variables to monitor. - run_length - - Returns - ------- - code : dict - The formatted code. - """ - if len(mon_vars) <= 0: - raise errors.ModelUseError(f'{self._name} has no monitor, cannot call this function.') - - # check indices # - for key, indices in mon_vars: - if indices is not None: - if isinstance(indices, list): - if not isinstance(indices[0], int): - raise errors.ModelUseError('Monitor index only supports list [int] or 1D array.') - elif isinstance(indices, np.ndarray): - if np.ndim(indices) != 1: - raise errors.ModelUseError('Monitor index only supports list [int] or 1D array.') - else: - raise errors.ModelUseError(f'Unknown monitor index type: {type(indices)}.') - - if profile.run_on_cpu(): - # monitor - mon = tools.DictPlus() - - code_scope = {self._name: self.ensemble, f'{self._name}_runner': self} - code_args, code_arg2call, code_lines = set(), {}, [] - - # generate code of monitor function - # --------------------------------- - mon_idx = 0 - for key, indices in mon_vars: - if indices is not None: - indices = np.asarray(indices) - attr_item = key.split('.') - - # get the code line # - if (len(attr_item) == 1) and (attr_item[0] not in self.ensemble.ST): - attr = attr_item[0] - self.check_attr(attr) - data = getattr(self.ensemble, attr) - if not isinstance(data, np.ndarray): - assert errors.ModelUseError(f'BrainPy only supports monitor of arrays.') - shape = data.shape - mon_name = f'mon_{attr}' - target_name = attr - if indices is None: - line = f'{mon_name}[_i] = {target_name}' - else: - idx_name = f'idx{mon_idx}_{attr}' - line = f'{mon_name}[_i] = {target_name}[{idx_name}]' - code_scope[idx_name] = indices - code_args.add(mon_name) - code_arg2call[mon_name] = f'{self._name}.mon["{key}"]' - code_args.add(target_name) - code_arg2call[target_name] = f'{self._name}.{attr}' - else: - if len(attr_item) == 1: - item, attr = attr_item[0], 'ST' - elif len(attr_item) == 2: - attr, item = attr_item - else: - raise errors.ModelUseError(f'Unknown target : {key}.') - data = getattr(self.ensemble, attr) - shape = data[item].shape - idx = data['_var2idx'][item] - mon_name = f'mon_{attr}_{item}' - target_name = attr - if indices is None: - line = f'{mon_name}[_i] = {target_name}[{idx}]' - else: - idx_name = f'idx{mon_idx}_{attr}_{item}' - line = f'{mon_name}[_i] = {target_name}[{idx}][{idx_name}]' - code_scope[idx_name] = indices - code_args.add(mon_name) - code_arg2call[mon_name] = f'{self._name}.mon["{key}"]' - code_args.add(target_name) - code_arg2call[target_name] = f'{self._name}.{attr}["_data"]' - mon_idx += 1 - - # initialize monitor array # - key = key.replace('.', '_') - if indices is None: - mon[key] = np.zeros((run_length,) + shape, dtype=np.float_) - else: - mon[key] = np.zeros((run_length, len(indices)) + shape[1:], dtype=np.float_) - - # add line # - code_lines.append(line) - - # final code - # ---------- - code_lines.insert(0, f'# "monitor" step function of {self._name}') - code_lines.append('\n') - code_args.add('_i') - code_arg2call['_i'] = '_i' - - # compile function - code_to_compile = [f'def monitor_step({tools.func_call(code_args)}):'] + code_lines - func_code = '\n '.join(code_to_compile) - - if not profile.is_merge_steps(): - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def {self._name}_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scope, ('__builtins__', 'monitor_step')) - - exec(compile(func_code, '', 'exec'), code_scope) - monitor_step = code_scope['monitor_step'] - # if profile.is_jit(): - # monitor_step = tools.jit(monitor_step) - self.monitor_step = monitor_step - - # format function call - arg2call = [code_arg2call[arg] for arg in sorted(list(code_args))] - func_call = f'{self._name}_runner.monitor_step({tools.func_call(arg2call)})' - - return mon, {'monitor': {'scopes': code_scope, - 'args': code_args, - 'arg2calls': code_arg2call, - 'codes': code_lines, - 'call': func_call}} - - else: - results = {} - mon = tools.DictPlus() - - # generate code of monitor function - # --------------------------------- - mon_idx = 0 - for key, indices in mon_vars: - if indices is not None: - indices = np.asarray(indices) - code_scope = {self._name: self.ensemble, f'{self._name}_runner': self} - code_args, code_arg2call, code_lines = set(), {}, [] - - attr_item = key.split('.') - key = key.replace(".", "_") - # get the code line # - if (len(attr_item) == 1) and (attr_item[0] not in self.ensemble.ST): - attr, item = attr_item[0], '' - self.check_attr(attr) - if not isinstance(getattr(self.ensemble, attr), np.ndarray): - assert errors.ModelUseError(f'BrainPy only supports monitor of arrays.') - data = getattr(self.ensemble, attr) - shape = data.shape - mon_name = f'mon_{attr}' - target_name = f'{attr}_cuda' - if indices is None: - num_data = shape[0] - line = f'{mon_name}[_i, cuda_i] = {target_name}[cuda_i]' - else: - num_data = len(indices) - idx_name = f'idx{mon_idx}_{attr}' - code_lines.append(f'mon_idx = {idx_name}[cuda_i]') - line = f'{mon_name}[_i, cuda_i] = {target_name}[mon_idx]' - code_scope[idx_name] = cuda.to_device(indices) - code_args.add(mon_name) - code_arg2call[mon_name] = f'{self._name}_runner.mon_{key}_cuda' - self.set_gpu_data(f'{attr}_cuda', data) - code_args.add(target_name) - code_arg2call[target_name] = f'{self._name}_runner.{attr}_cuda' - else: - if len(attr_item) == 1: - item, attr = attr_item[0], 'ST' - elif len(attr_item) == 2: - attr, item = attr_item - else: - raise errors.ModelUseError(f'Unknown target : {key}.') - data = getattr(self.ensemble, attr) - shape = data[item].shape - idx = getattr(self.ensemble, attr)['_var2idx'][item] - mon_name = f'mon_{attr}_{item}' - target_name = attr - if indices is None: - num_data = shape[0] - line = f'{mon_name}[_i, cuda_i] = {target_name}[{idx}, cuda_i]' - else: - num_data = len(indices) - idx_name = f'idx{mon_idx}_{attr}_{item}' - code_lines.append(f'mon_idx = {idx_name}[cuda_i]') - line = f'{mon_name}[_i, cuda_i] = {target_name}[{idx}, mon_idx]' - code_scope[idx_name] = cuda.to_device(indices) - code_args.add(mon_name) - code_arg2call[mon_name] = f'{self._name}_runner.mon_{key}_cuda' - self.set_gpu_data(f'{attr}_cuda', data) - code_args.add(target_name) - code_arg2call[target_name] = f'{self._name}_runner.{attr}_cuda' - - # initialize monitor array # - if indices is None: - mon[key] = np.zeros((run_length,) + shape, dtype=np.float_) - else: - mon[key] = np.zeros((run_length, num_data) + shape[1:], dtype=np.float_) - self.set_gpu_data(f'mon_{key}_cuda', mon[key]) - - # add line # - code_args.add('_i') - code_arg2call['_i'] = '_i' - code_scope['cuda'] = cuda - - # final code - # ---------- - code_lines.append(line) - code_lines = [' ' + line for line in code_lines] - code_lines.insert(0, f'if cuda_i < {num_data}:') - - # compile function - func_name = f'monitor_of_{attr}_{item}' - code_to_compile = [f'# "monitor" of {self._name}.{attr}.{item}', - f'def {func_name}({tools.func_call(code_args)}):', - f' cuda_i = cuda.grid(1)'] - code_to_compile += [f' {line}' for line in code_lines] - func_code = '\n'.join(code_to_compile) - exec(compile(func_code, '', 'exec'), code_scope) - monitor_step = code_scope[func_name] - monitor_step = cuda.jit(monitor_step) - setattr(self, func_name, monitor_step) - - if not profile.is_merge_steps(): - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def {self._name}_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scope, ('__builtins__', 'monitor_step')) - - # format function call - if num_data <= profile.get_num_thread_gpu(): - num_thread = num_data - num_block = 1 - else: - num_thread = profile.get_num_thread_gpu() - num_block = math.ceil(num_data / profile.get_num_thread_gpu()) - arg2call = [code_arg2call[arg] for arg in sorted(list(code_args))] - func_call = f'{self._name}_runner.{func_name}[{num_block}, {num_thread}]({tools.func_call(arg2call)})' - - results[f'monitor-{mon_idx}'] = {'scopes': code_scope, - 'args': code_args, - 'arg2calls': code_arg2call, - 'codes': code_lines, - 'call': func_call, - 'num_data': num_data} - - mon_idx += 1 - - return mon, results - - def get_codes_of_steps(self): - """Get the code of user defined update steps. - - Returns - ------- - code : dict - The formatted code. - """ - if self._model.mode == constants.SCALAR_MODE: - return self.step_scalar_model() - else: - return self.step_vector_model() - - def format_step_code(self, func_code): - """Format code of user defined step function. - - Parameters - ---------- - func_code : str - The user defined function codes. - """ - tree = ast.parse(func_code.strip()) - formatter = tools.CodeLineFormatter() - formatter.visit(tree) - return formatter - - def merge_integrators(self, func): - """Substitute the user defined integrators into the main step functions. - - Parameters - ---------- - func : callable - The user defined (main) step function. - - Returns - ------- - results : tuple - The codes and code scope. - """ - # get code and code lines - func_code = tools.deindent(tools.get_main_code(func)) - formatter = self.format_step_code(func_code) - code_lines = formatter.lines - - # get function scope - vars = inspect.getclosurevars(func) - code_scope = dict(vars.nonlocals) - code_scope.update(vars.globals) - code_scope.update({self._name: self.ensemble}) - code_scope.update(formatter.scope) - if len(code_lines) == 0: - return '', code_scope - - # code scope update - scope_to_add = {} - scope_to_del = set() - need_add_mapping_scope = False - for k, v in code_scope.items(): - if isinstance(v, integration.Integrator): - if profile.is_merge_integrators(): - need_add_mapping_scope = True - - # locate the integration function - need_replace = False - int_func_name = v.py_func_name - for line_no, line in enumerate(code_lines): - if int_func_name in tools.get_identifiers(line): - need_replace = True - break - if not need_replace: - scope_to_del.add(k) - continue - - # get integral function line indent - line_indent = tools.get_line_indent(line) - indent = ' ' * line_indent - - # get the replace line and arguments need to replace - new_line, args, kwargs = tools.replace_func(line, int_func_name) - # append code line of argument replacement - func_args = v.diff_eq.func_args - append_lines = [indent + f'_{func_args[i]} = {args[i]}' for i in range(len(args))] - for arg in func_args[len(args):]: - append_lines.append(indent + f'_{arg} = {kwargs[arg]}') - - # append numerical integration code lines - append_lines.extend([indent + l for l in v.update_code.split('\n')]) - append_lines.append(indent + new_line) - - # add appended lines into the main function code lines - code_lines = code_lines[:line_no] + append_lines + code_lines[line_no + 1:] - - # get scope variables to delete - scope_to_del.add(k) - for k_, v_ in v.code_scope.items(): - if profile.is_jit() and callable(v_): - v_ = tools.numba_func(v_, params=self._pars.updates) - scope_to_add[k_] = v_ - - else: - if self._model.mode == constants.SCALAR_MODE: - for ks, vs in utils.get_func_scope(v.update_func, include_dispatcher=True).items(): - if ks in self._pars.heters: - raise errors.ModelUseError( - f'Heterogeneous parameter "{ks}" is not in step functions, ' - f'it will not work. Please set "brainpy.profile.set(merge_integrators=True)" ' - f'to try to merge parameter "{ks}" into the step functions.') - if profile.is_jit(): - code_scope[k] = tools.numba_func(v.update_func, params=self._pars.updates) - - elif type(v).__name__ == 'function': - if profile.is_jit(): - code_scope[k] = tools.numba_func(v, params=self._pars.updates) - - # update code scope - if need_add_mapping_scope: - code_scope.update(integration.get_mapping_scope()) - code_scope.update(scope_to_add) - for k in scope_to_del: - code_scope.pop(k) - - # return code lines and code scope - return '\n'.join(code_lines), code_scope, formatter - - def step_vector_model(self): - results = dict() - - # check whether the model include heterogeneous parameters - delay_keys = self._delay_keys - - for func in self._steps: - # information about the function - func_name = func.__name__ - stripped_fname = tools.get_func_name(func, replace=True) - func_args = inspect.getfullargspec(func).args - - # initialize code namespace - used_args, code_arg2call = set(), {} - func_code, code_scope, formatter = self.merge_integrators(func) - code_scope[f'{self._name}_runner'] = self - - # check function code - try: - states = {k: getattr(self.ensemble, k) for k in func_args - if k not in constants.ARG_KEYWORDS and - isinstance(getattr(self.ensemble, k), types.ObjState)} - except AttributeError: - raise errors.ModelUseError(f'Model "{self._name}" does not have all the ' - f'required attributes: {func_args}.') - add_args = set() - for i, arg in enumerate(func_args): - used_args.add(arg) - if len(states) == 0: - continue - if arg in states: - st = states[arg] - var2idx = st['_var2idx'] - - if self.ensemble._is_state_attr(arg): - # Function with "delayed" decorator should use - # ST pulled from the delay queue - if func_name.startswith('_brainpy_delayed_'): - if len(delay_keys): - dout = f'{arg}_dout' - add_args.add(dout) - code_arg2call[dout] = f'{self._name}.{arg}._delay_out' - for st_k in delay_keys: - p = f'{arg}\[([\'"]{st_k}[\'"])\]' - r = f"{arg}[{var2idx['_' + st_k + '_offset']} + {dout}]" - func_code = re.sub(r'' + p, r, func_code) - else: - # Function without "delayed" decorator should push their - # updated ST to the delay queue - if len(delay_keys): - func_code_left = '\n'.join(formatter.lefts) - func_keys = set(re.findall(r'' + arg + r'\[[\'"](\w+)[\'"]\]', func_code_left)) - func_delay_keys = func_keys.intersection(delay_keys) - if len(func_delay_keys) > 0: - din = f'{arg}_din' - add_args.add(din) - code_arg2call[din] = f'{self._name}.{arg}._delay_in' - for st_k in func_delay_keys: - right = f'{arg}[{var2idx[st_k]}]' - left = f"{arg}[{var2idx['_' + st_k + '_offset']} + {din}]" - func_code += f'\n{left} = {right}' - - # replace key access to index access - for st_k in st._keys: - p = f'{arg}\[([\'"]{st_k}[\'"])\]' - r = f"{arg}[{var2idx[st_k]}]" - func_code = re.sub(r'' + p, r, func_code) - - # substitute arguments - code_args = add_args - for arg in used_args: - if arg in constants.ARG_KEYWORDS: - code_arg2call[arg] = arg - else: - if isinstance(getattr(self.ensemble, arg), types.ObjState): - code_arg2call[arg] = f'{self._name}.{arg}["_data"]' - else: - code_arg2call[arg] = f'{self._name}.{arg}' - code_args.add(arg) - - # substitute "range" to "numba.prange" - arg_substitute = {} - if ' range' in func_code: - arg_substitute['range'] = 'numba.prange' - code_scope['numba'] = numba - func_code = tools.word_replace(func_code, arg_substitute) - - # update code scope - for k in list(code_scope.keys()): - if k in self._pars.updates: - code_scope[k] = self._pars.updates[k] - - # handle the "_normal_like_" - func_code = NoiseHandler.normal_pattern.sub(NoiseHandler.vector_replace_f, func_code) - code_scope['numpy'] = np - - # final - code_lines = func_code.split('\n') - code_lines.insert(0, f'# "{stripped_fname}" step function of {self._name}') - code_lines.append('\n') - - # code to compile - code_to_compile = [f'def {stripped_fname}({tools.func_call(code_args)}):'] - code_to_compile += code_lines - func_code = '\n '.join(code_to_compile) - exec(compile(func_code, '', 'exec'), code_scope) - func = code_scope[stripped_fname] - if profile.is_jit(): - func = tools.jit(func) - if not profile.is_merge_steps(): - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def {self._name}_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scope, ['__builtins__', stripped_fname]) - - # set the function to the model - setattr(self, stripped_fname, func) - # function call - arg2calls = [code_arg2call[arg] for arg in sorted(list(code_args))] - func_call = f'{self._name}_runner.{stripped_fname}({tools.func_call(arg2calls)})' - - results[stripped_fname] = {'scopes': code_scope, - 'args': code_args, - 'arg2calls': code_arg2call, - 'codes': code_lines, - 'call': func_call} - - return results - - def step_scalar_model(self): - results = dict() - - # check whether the model include heterogeneous parameters - delay_keys = self._delay_keys - all_heter_pars = set(self._pars.heters.keys()) - - for i, func in enumerate(self._steps): - func_name = func.__name__ - - # get necessary code data - # ----------------------- - # 1. code arguments - # 2. code argument_to_call - # 3. code lines - # 4. code scope variables - used_args, code_arg2call = set(), {} - func_args = inspect.getfullargspec(func).args - func_code, code_scope, formatter = self.merge_integrators(func) - code_scope[f'{self._name}_runner'] = self - try: - states = {k: getattr(self.ensemble, k) for k in func_args - if k not in constants.ARG_KEYWORDS and - isinstance(getattr(self.ensemble, k), types.ObjState)} - except AttributeError: - raise errors.ModelUseError(f'Model "{self._name}" does not have all the ' - f'required attributes: {func_args}.') - - # update functions in code scope - # 1. recursively jit the function - # 2. update the function parameters - for k, v in code_scope.items(): - if profile.is_jit() and callable(v): - code_scope[k] = tools.numba_func(func=v, params=self._pars.updates) - - add_args = set() - # substitute STATE item access to index - for i, arg in enumerate(func_args): - used_args.add(arg) - if len(states) == 0: - continue - if arg not in states: - continue - - st = states[arg] - var2idx = st['_var2idx'] - if self.ensemble._is_state_attr(arg): - if func_name.startswith('_brainpy_delayed_'): - if len(delay_keys): - dout = f'{arg}_dout' - add_args.add(dout) - code_arg2call[dout] = f'{self._name}.{arg}._delay_out' - # Function with "delayed" decorator should use ST pulled from the delay queue - for st_k in delay_keys: - p = f'{arg}\[([\'"]{st_k}[\'"])\]' - r = f"{arg}[{var2idx['_' + st_k + '_offset']} + {dout}, _obj_i]" - func_code = re.sub(r'' + p, r, func_code) - else: - if len(delay_keys): - # Function without "delayed" decorator should push - # their updated ST to the delay queue - func_code_left = '\n'.join(formatter.lefts) - func_keys = set(re.findall(r'' + arg + r'\[[\'"](\w+)[\'"]\]', func_code_left)) - func_delay_keys = func_keys.intersection(delay_keys) - if len(func_delay_keys) > 0: - din = f'{arg}_din' - add_args.add(din) - code_arg2call[din] = f'{self._name}.{arg}._delay_in' - for st_k in func_delay_keys: - right = f'{arg}[{var2idx[st_k]}, _obj_i]' - left = f"{arg}[{var2idx['_' + st_k + '_offset']} + {din}, _obj_i]" - func_code += f'\n{left} = {right}' - for st_k in st._keys: - p = f'{arg}\[([\'"]{st_k}[\'"])\]' - r = f"{arg}[{var2idx[st_k]}, _obj_i]" - func_code = re.sub(r'' + p, r, func_code) - elif arg == 'pre': - # 1. implement the atomic operations for "pre" - if profile.run_on_gpu(): - code_lines = func_code.split('\n') - add_cuda = False - line_no = 0 - while line_no < len(code_lines): - line = code_lines[line_no] - blank_no = len(line) - len(line.lstrip()) - line = line.strip() - if line.startswith('pre'): - pre_transformer = tools.find_atomic_op(line, var2idx) - if pre_transformer.left is not None: - left = pre_transformer.left - right = pre_transformer.right - code_lines[line_no] = ' ' * blank_no + f'cuda.atomic.add({left}, _pre_i, {right})' - add_cuda = True - line_no += 1 - if add_cuda: - code_scope['cuda'] = cuda - func_code = '\n'.join(code_lines) - # 2. transform the key access to index access - for st_k in st._keys: - p = f'pre\[([\'"]{st_k}[\'"])\]' - r = f"pre[{var2idx[st_k]}, _pre_i]" - func_code = re.sub(r'' + p, r, func_code) - elif arg == 'post': - # 1. implement the atomic operations for "post" - if profile.run_on_gpu(): - code_lines = func_code.split('\n') - add_cuda = False - line_no = 0 - while line_no < len(code_lines): - line = code_lines[line_no] - blank_no = len(line) - len(line.lstrip()) - line = line.strip() - if line.startswith('post'): - post_transformer = tools.find_atomic_op(line, var2idx) - if post_transformer.left is not None: - left = post_transformer.left - right = post_transformer.right - code_lines[line_no] = ' ' * blank_no + f'cuda.atomic.add({left}, _post_i, {right})' - add_cuda = True - line_no += 1 - if add_cuda: - code_scope['cuda'] = cuda - func_code = '\n'.join(code_lines) - # 2. transform the key access to index access - for st_k in st._keys: - p = f'post\[([\'"]{st_k}[\'"])\]' - r = f"post[{var2idx[st_k]}, _post_i]" - func_code = re.sub(r'' + p, r, func_code) - else: - raise ValueError - - # get formatted function arguments - # -------------------------------- - # 1. For argument in "ARG_KEYWORDS", keep it unchanged - # 2. For argument is an instance of ObjState, get it's cuda data - # 3. For other argument, get it's cuda data - code_args = add_args - for arg in used_args: - if arg in constants.ARG_KEYWORDS: - code_arg2call[arg] = arg - else: - data = getattr(self.ensemble, arg) - if profile.run_on_cpu(): - if isinstance(data, types.ObjState): - code_arg2call[arg] = f'{self._name}.{arg}["_data"]' - else: - code_arg2call[arg] = f'{self._name}.{arg}' - else: - if isinstance(data, types.ObjState): - code_arg2call[arg] = f'{self._name}_runner.{arg}_cuda' - else: - code_arg2call[arg] = f'{self._name}_runner.{arg}_cuda' - self.set_gpu_data(f'{arg}_cuda', data) - code_args.add(arg) - - # add the for loop in the start of the main code - has_pre = 'pre' in func_args - has_post = 'post' in func_args - if profile.run_on_cpu(): - code_lines = [f'for _obj_i in numba.prange({self.ensemble.num}):'] - code_scope['numba'] = numba - else: - code_lines = [f'_obj_i = cuda.grid(1)', - f'if _obj_i < {self.ensemble.num}:'] - code_scope['cuda'] = cuda - - if has_pre: - code_args.add(f'pre_ids') - code_arg2call[f'pre_ids'] = f'{self._name}_runner.pre_ids' - code_lines.append(f' _pre_i = pre_ids[_obj_i]') - self.set_data('pre_ids', getattr(self.ensemble, 'pre_ids')) - if has_post: - code_args.add(f'post_ids') - code_arg2call[f'post_ids'] = f'{self._name}_runner.post_ids' - code_lines.append(f' _post_i = post_ids[_obj_i]') - self.set_data('post_ids', getattr(self.ensemble, 'post_ids')) - - # substitute heterogeneous parameter "p" to "p[_obj_i]" - # ------------------------------------------------------ - arg_substitute = {} - for p in self._pars.heters.keys(): - if p in code_scope: - arg_substitute[p] = f'{p}[_obj_i]' - if len(arg_substitute): - func_code = tools.word_replace(func_code, arg_substitute) - - # add the main code (user defined) - # ------------------ - for l in func_code.split('\n'): - code_lines.append(' ' + l) - code_lines.append('\n') - stripped_fname = tools.get_func_name(func, replace=True) - code_lines.insert(0, f'# "{stripped_fname}" step function of {self._name}') - - # update code scope - # ------------------ - for k in list(code_scope.keys()): - if k in self._pars.updates: - if profile.run_on_cpu(): - # run on cpu : - # 1. update the parameter - # 2. remove the heterogeneous parameter - code_scope[k] = self._pars.updates[k] - if k in all_heter_pars: - all_heter_pars.remove(k) - else: - # run on gpu : - # 1. update the parameter - # 2. transform the heterogeneous parameter to function argument - if k in all_heter_pars: - code_args.add(k) - code_arg2call[k] = cuda.to_device(self._pars.updates[k]) - else: - code_scope[k] = self._pars.updates[k] - - # handle the "_normal_like_" - # --------------------------- - func_code = '\n'.join(code_lines) - if len(NoiseHandler.normal_pattern.findall(func_code)): - if profile.run_on_gpu(): # gpu noise - func_code = NoiseHandler.normal_pattern.sub(NoiseHandler.cuda_replace_f, func_code) - code_scope['xoroshiro128p_normal_float64'] = xoroshiro128p_normal_float64 - num_block, num_thread = tools.get_cuda_size(self.ensemble.num) - code_args.add('rng_states') - code_arg2call['rng_states'] = f'{self._name}_runner.rng_states' - rng_state = create_xoroshiro128p_states(num_block * num_thread, seed=np.random.randint(100000)) - setattr(self, 'rng_states', rng_state) - else: # cpu noise - func_code = NoiseHandler.normal_pattern.sub(NoiseHandler.scalar_replace_f, func_code) - code_scope['numpy'] = np - code_lines = func_code.split('\n') - - # code to compile - # ----------------- - # 1. get the codes to compile - code_to_compile = [f'def {stripped_fname}({tools.func_call(code_args)}):'] - code_to_compile += code_lines - func_code = '\n '.join(code_to_compile) - exec(compile(func_code, '', 'exec'), code_scope) - # 2. output the function codes - if not profile.is_merge_steps(): - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def {self._name}_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scope, ['__builtins__', stripped_fname]) - # 3. jit the compiled function - func = code_scope[stripped_fname] - if profile.run_on_cpu(): - if profile.is_jit(): - func = tools.jit(func) - else: - func = cuda.jit(func) - # 4. set the function to the model - setattr(self, stripped_fname, func) - - # get function call - # ----------------- - # 1. get the functional arguments - arg2calls = [code_arg2call[arg] for arg in sorted(list(code_args))] - arg_code = tools.func_call(arg2calls) - if profile.run_on_cpu(): - # 2. function call on cpu - func_call = f'{self._name}_runner.{stripped_fname}({arg_code})' - else: - # 3. function call on gpu - num_block, num_thread = tools.get_cuda_size(self.ensemble.num) - func_call = f'{self._name}_runner.{stripped_fname}[{num_block}, {num_thread}]({arg_code})' - - # the final result - # ------------------ - results[stripped_fname] = {'scopes': code_scope, - 'args': code_args, - 'arg2calls': code_arg2call, - 'codes': code_lines, - 'call': func_call, - 'num_data': self.ensemble.num} - - # WARNING: heterogeneous parameter may not in the main step functions - if len(all_heter_pars) > 0: - raise errors.ModelDefError(f''' -Heterogeneous parameters "{list(all_heter_pars)}" are not defined -in main step function. BrainPy can not recognize. - -This error may be caused by: -1. Heterogeneous par is defined in other non-main step functions. -2. Heterogeneous par is defined in "integrators", but do not call - "profile.set(merge_integrators=True)". - -Several ways to correct this error is: -1. Define the heterogeneous parameter in the "ST". -2. Call "profile.set(merge_integrators=True)" define the network definition. - -''') - - return results - - def merge_codes(self, compiled_result): - codes_of_calls = [] # call the compiled functions - - if profile.run_on_cpu(): - if profile.is_merge_steps(): - lines, code_scopes, args, arg2calls = [], dict(), set(), dict() - for item in self.get_schedule(): - if item in compiled_result: - lines.extend(compiled_result[item]['codes']) - code_scopes.update(compiled_result[item]['scopes']) - args = args | compiled_result[item]['args'] - arg2calls.update(compiled_result[item]['arg2calls']) - - args = sorted(list(args)) - arg2calls_list = [arg2calls[arg] for arg in args] - lines.insert(0, f'\n# {self._name} "merge_func"' - f'\ndef merge_func({tools.func_call(args)}):') - func_code = '\n '.join(lines) - exec(compile(func_code, '', 'exec'), code_scopes) - - func = code_scopes['merge_func'] - if profile.is_jit(): - func = tools.jit(func) - self.merge_func = func - func_call = f'{self._name}_runner.merge_func({tools.func_call(arg2calls_list)})' - codes_of_calls.append(func_call) - - if profile.show_format_code(): - utils.show_code_str(func_code.replace('def ', f'def {self._name}_')) - if profile.show_code_scope(): - utils.show_code_scope(code_scopes, ('__builtins__', 'merge_func')) - - else: - for item in self.get_schedule(): - if item in compiled_result: - func_call = compiled_result[item]['call'] - codes_of_calls.append(func_call) - - else: - if profile.is_merge_steps(): - print('WARNING: GPU mode do not support to merge steps.') - - for item in self.get_schedule(): - for compiled_key in compiled_result.keys(): - if compiled_key.startswith(item): - func_call = compiled_result[compiled_key]['call'] - codes_of_calls.append(func_call) - codes_of_calls.append('cuda.synchronize()') - - return codes_of_calls - - def get_schedule(self): - return self._schedule - - def set_schedule(self, schedule): - if not isinstance(schedule, (list, tuple)): - raise errors.ModelUseError('"schedule" must be a list/tuple.') - all_func_names = ['input', 'monitor'] + self._step_names - for s in schedule: - if s not in all_func_names: - raise errors.ModelUseError(f'Unknown step function "{s}" for model "{self._name}".') - self._schedule = schedule - - def set_data(self, key, data): - if profile.run_on_gpu(): - if np.isscalar(data): - data_cuda = data - else: - data_cuda = cuda.to_device(data) - setattr(self, key, data_cuda) - else: - setattr(self, key, data) - - def set_gpu_data(self, key, val): - if key not in self.gpu_data: - if isinstance(val, np.ndarray): - val = cuda.to_device(val) - elif isinstance(val, types.ObjState): - val = val.get_cuda_data() - setattr(self, key, val) - self.gpu_data[key] = val - - def gpu_data_to_cpu(self): - for val in self.gpu_data.values(): - val.to_host() - - -class TrajectoryRunner(Runner): - """Runner class for trajectory. - - Parameters - ---------- - ensemble : NeuGroup - The neuron ensemble. - target_vars : tuple, list - The targeted variables for trajectory. - fixed_vars : dict - The fixed variables. - """ - - def __init__(self, ensemble, target_vars, fixed_vars=None): - # check ensemble - from brainpy.core.neurons import NeuGroup - if not isinstance(ensemble, NeuGroup): - raise errors.ModelUseError(f'{self.__name__} only supports the instance of NeuGroup.') - - # initialization - super(TrajectoryRunner, self).__init__(ensemble=ensemble) - - # check targeted variables - if not isinstance(target_vars, (list, tuple)): - raise errors.ModelUseError('"target_vars" must be a list/tuple.') - for var in target_vars: - if var not in self._model.variables: - raise errors.ModelUseError(f'"{var}" in "target_vars" is not defined in model "{self._model.name}".') - self.target_vars = target_vars - - # check fixed variables - try: - if fixed_vars is not None: - isinstance(fixed_vars, dict) - else: - fixed_vars = dict() - except AssertionError: - raise errors.ModelUseError('"fixed_vars" must be a dict.') - self.fixed_vars = dict() - for integrator in self._model.integrators: - var_name = integrator.diff_eq.var_name - if var_name not in target_vars: - if var_name in fixed_vars: - self.fixed_vars[var_name] = fixed_vars.get(var_name) - else: - self.fixed_vars[var_name] = self._model.variables.get(var_name) - for var in fixed_vars.keys(): - if var not in self.fixed_vars: - self.fixed_vars[var] = fixed_vars.get(var) - - def format_step_code(self, func_code): - """Format code of user defined step function. - - Parameters - ---------- - func_code : str - The user defined function. - """ - tree = ast.parse(func_code.strip()) - formatter = tools.LineFormatterForTrajectory(self.fixed_vars) - formatter.visit(tree) - return formatter diff --git a/brainpy/core/synapses.py b/brainpy/core/synapses.py deleted file mode 100644 index 1605becf..00000000 --- a/brainpy/core/synapses.py +++ /dev/null @@ -1,243 +0,0 @@ -# -*- coding: utf-8 -*- - -import re - -import numpy as np - -from . import base -from . import constants -from . import neurons -from .. import connectivity -from .. import errors -from .. import profile -from .. import tools - -__all__ = [ - 'SynType', - 'SynConn', - 'delayed', -] - -_SYN_CONN_NO = 0 - - -class SynType(base.ObjType): - """Abstract Synapse Type. - - It can be defined based on a collection of synapses or a single synapse model. - """ - - def __init__(self, name, ST, steps, mode='vector', requires=None, hand_overs=None, ): - if mode not in [constants.SCALAR_MODE, constants.VECTOR_MODE, constants.MATRIX_MODE]: - raise errors.ModelDefError('SynType only support "scalar", "vector" or "matrix".') - - super(SynType, self).__init__(ST=ST, - requires=requires, - steps=steps, - name=name, - mode=mode, - hand_overs=hand_overs) - - # inspect delay keys - # ------------------ - - # delay function - delay_funcs = [] - for func in self.steps: - if func.__name__.startswith('_brainpy_delayed_'): - delay_funcs.append(func) - if len(delay_funcs): - delay_func_code = '\n'.join([tools.deindent(tools.get_main_code(func)) for func in delay_funcs]) - delay_func_code_left = '\n'.join(tools.format_code(delay_func_code).lefts) - - # get delayed variables - _delay_keys = set() - delay_keys_in_left = set(re.findall(r'ST\[[\'"](\w+)[\'"]\]', delay_func_code_left)) - if len(delay_keys_in_left) > 0: - raise errors.ModelDefError(f'Delayed function cannot assign value to "ST".') - delay_keys = set(re.findall(r'ST\[[\'"](\w+)[\'"]\]', delay_func_code)) - if len(delay_keys) > 0: - _delay_keys.update(delay_keys) - self._delay_keys = list(_delay_keys) - - -class SynConn(base.Ensemble): - """Synaptic connections. - - Parameters - ---------- - model : SynType - The instantiated neuron type model. - pars_update : dict - Parameters to update. - pre_group : neurons.NeuGroup, neurons.NeuSubGroup - Pre-synaptic neuron group. - post_group : neurons.NeuGroup, neurons.NeuSubGroup - Post-synaptic neuron group. - conn : connectivity.Connector - Connection method to create synaptic connectivity. - num : int - The number of the synapses. - delay : float - The time of the synaptic delay. - monitors : list, tuple - Variables to monitor. - name : str - The name of the neuron group. - """ - - def __init__(self, model, pre_group=None, post_group=None, conn=None, delay=0., - name=None, monitors=None, satisfies=None, pars_update=None, ): - # name - # ---- - if name is None: - global _SYN_CONN_NO - name = f'SynConn{_SYN_CONN_NO}' - _SYN_CONN_NO += 1 - else: - name = name - - # model - # ------ - if not isinstance(model, SynType): - raise errors.ModelUseError(f'{type(self).__name__} receives an instance of {SynType.__name__}, ' - f'not {type(model).__name__}.') - - if model.mode == 'scalar': - if pre_group is None or post_group is None: - raise errors.ModelUseError('Using scalar-based synapse model must ' - 'provide "pre_group" and "post_group".') - - # pre or post neuron group - # ------------------------ - self.pre_group = pre_group - self.post_group = post_group - self.conn = None - num = 1 - if pre_group is not None and post_group is not None: - # check - # ------ - if not isinstance(pre_group, (neurons.NeuGroup, neurons.NeuSubGroup)): - raise errors.ModelUseError('"pre_group" must be an instance of NeuGroup/NeuSubGroup.') - if not isinstance(post_group, (neurons.NeuGroup, neurons.NeuSubGroup)): - raise errors.ModelUseError('"post_group" must be an instance of NeuGroup/NeuSubGroup.') - - # pre and post synaptic state - self.pre = pre_group.ST - self.post = post_group.ST - - if conn is not None: - # connections - # ------------ - if isinstance(conn, connectivity.Connector): - self.conn = conn - self.conn(pre_group.indices, post_group.indices) - else: - if isinstance(conn, np.ndarray): - # check matrix dimension - if np.ndim(conn) != 2: - raise errors.ModelUseError(f'"conn" must be a 2D array, not {np.ndim(conn)}D.') - # check matrix shape - conn_shape = np.shape(conn) - if not (conn_shape[0] == pre_group.num and conn_shape[1] == post_group.num): - raise errors.ModelUseError( - f'The shape of "conn" must be ({pre_group.num}, {post_group.num})') - # get pre_ids and post_ids - pre_ids, post_ids = np.where(conn > 0) - else: - # check conn type - if not isinstance(conn, dict): - raise errors.ModelUseError(f'"conn" only support "dict", 2D ndarray, ' - f'or instance of bp.connect.Connector.') - # check conn content - if not ('i' in conn and 'j' in conn): - raise errors.ModelUseError('When provided "conn" is a dict, "i" and "j" must in "conn".') - # get pre_ids and post_ids - pre_ids = np.asarray(conn['i'], dtype=np.int_) - post_ids = np.asarray(conn['j'], dtype=np.int_) - self.conn = connectivity.Connector() - self.conn.pre_ids = pre_group.indices.flatten()[pre_ids] - self.conn.post_ids = post_group.indices.flatten()[post_ids] - - # get synaptic structures - self.conn.set_size(num_post=post_group.size, num_pre=pre_group.size) - if model.mode == constants.SCALAR_MODE: - self.conn.set_requires(model.step_args + ['post2syn', 'pre2syn']) - else: - self.conn.set_requires(model.step_args) - for k in self.conn.requires: - setattr(self, k, getattr(self.conn, k)) - self.pre_ids = self.conn.pre_ids - self.post_ids = self.conn.post_ids - num = len(self.pre_ids) - - if satisfies is not None and 'num' in satisfies: - num = satisfies['num'] - - try: - assert 0 < num < 2 ** 64 - except AssertionError: - raise errors.ModelUseError('Total synapse number "num" must be a valid number in "uint64".') - - # initialize - # ---------- - super(SynConn, self).__init__(model=model, - pars_update=pars_update, - name=name, - num=num, - monitors=monitors, - cls_type=constants.SYN_CONN_TYPE, - satisfies=satisfies) - - # delay - # ------- - if delay is None: - delay_len = 1 - elif isinstance(delay, (int, float)): - dt = profile.get_dt() - delay_len = int(np.ceil(delay / dt)) - if delay_len == 0: - delay_len = 1 - else: - raise ValueError("BrainPy currently doesn't support other kinds of delay.") - self.delay_len = delay_len # delay length - - # ST - # -- - if self.model.mode == constants.MATRIX_MODE: - if pre_group is None: - if 'pre_size' not in satisfies: - raise errors.ModelUseError('"pre_size" must be provided in "satisfies" when "pre_group" is none.') - pre_size = satisfies['pre_size'] - else: - pre_size = pre_group.size - - if post_group is None: - if 'post_size' not in satisfies: - raise errors.ModelUseError('"post_size" must be provided in "satisfies" when "post_group" is none.') - post_size = satisfies['post_size'] - else: - post_size = post_group.size - size = (pre_size, post_size) - else: - size = (self.num,) - self.ST = self.model.ST.make_copy(size=size, - delay=delay_len, - delay_vars=self.model._delay_keys) - - -def delayed(func): - """Decorator for synapse delay. - - Parameters - ---------- - func : callable - The step function which use delayed synapse state. - - Returns - ------- - func : callable - The modified step function. - """ - func.__name__ = f'_brainpy_delayed_{func.__name__}' - return func diff --git a/brainpy/core/types.py b/brainpy/core/types.py deleted file mode 100644 index 867b8f01..00000000 --- a/brainpy/core/types.py +++ /dev/null @@ -1,442 +0,0 @@ -# -*- coding: utf-8 -*- - -import math -from collections import OrderedDict - -import numba as nb -import numpy as np -from numba import cuda - -from .. import errors -from .. import profile - -__all__ = [ - 'TypeChecker', - 'ObjState', - 'NeuState', - 'SynState', - 'gpu_set_vector_val', - 'ListConn', - 'MatConn', - 'Array', - 'Int', - 'Float', - 'List', - 'Dict', -] - - -class TypeChecker(object): - def __init__(self, help): - self.help = help - - def check(self, cls): - raise NotImplementedError - - @classmethod - def make_copy(cls, *args, **kwargs): - raise NotImplementedError - - -def gpu_set_scalar_val(data, val, idx): - i = cuda.grid(1) - if i < data.shape[1]: - data[idx, i] = val - - -def gpu_set_vector_val(data, val, idx): - i = cuda.grid(1) - if i < data.shape[1]: - data[idx, i] = val[i] - - -if cuda.is_available(): - gpu_set_scalar_val = cuda.jit('(float64[:, :], float64, int64)')(gpu_set_scalar_val) - gpu_set_vector_val = cuda.jit('(float64[:, :], float64[:], int64)')(gpu_set_vector_val) - - -class ObjState(dict, TypeChecker): - def __init__(self, *args, help='', **kwargs): - # 1. initialize TypeChecker - TypeChecker.__init__(self, help=help) - - # 2. get variables - variables = OrderedDict() - for a in args: - if isinstance(a, str): - variables[a] = 0. - elif isinstance(a, (tuple, list)): - for v in a: - variables[v] = 0. - elif isinstance(a, dict): - for key, val in a.items(): - if not isinstance(val, (int, float)): - raise ValueError(f'The default value setting in a dict must be int/float.') - variables[key] = val - else: - raise ValueError(f'Only support str/tuple/list/dict, not {type(variables)}.') - for key, val in kwargs.items(): - if not isinstance(val, (int, float)): - raise ValueError(f'The default value setting must be int/float.') - variables[key] = val - - # 3. others - self._keys = list(variables.keys()) - self._values = list(variables.values()) - self._vars = variables - - def check(self, cls): - if not isinstance(cls, type(self)): - raise errors.TypeMismatchError(f'Must be an instance of "{type(self)}", but got "{type(cls)}".') - for k in self._keys: - if k not in cls: - raise errors.TypeMismatchError(f'Key "{k}" is not found in "cls".') - - def get_cuda_data(self): - _data_cuda = self.__getitem__('_data_cuda') - if _data_cuda is None: - _data = self.__getitem__('_data') - _data_cuda = cuda.to_device(_data) - super(ObjState, self).__setitem__('_data_cuda', _data_cuda) - return _data_cuda - - def __setitem__(self, key, val): - if key in self._vars: - # get data - data = self.__getitem__('_data') - _var2idx = self.__getitem__('_var2idx') - idx = _var2idx[key] - # gpu setattr - if profile.run_on_gpu(): - gpu_data = self.get_cuda_data() - if data.shape[1] <= profile._num_thread_gpu: - num_thread = data.shape[1] - num_block = 1 - else: - num_thread = profile._num_thread_gpu - num_block = math.ceil(data.shape[1] / profile._num_thread_gpu) - if np.isscalar(val): - gpu_set_scalar_val[num_block, num_thread](gpu_data, val, idx) - else: - if val.shape[0] != data.shape[1]: - raise ValueError(f'Wrong value dimension {val.shape[0]} != {data.shape[1]}') - gpu_set_vector_val[num_block, num_thread](gpu_data, val, idx) - cuda.synchronize() - # cpu setattr - else: - data[idx] = val - elif key in ['_data', '_var2idx', '_idx2var']: - raise KeyError(f'"{key}" cannot be modified.') - else: - raise KeyError(f'"{key}" is not defined in {type(self).__name__}, ' - f'only finds "{str(self._keys)}".') - - def __str__(self): - return f'{self.__class__.__name__} ({str(self._keys)})' - - def __repr__(self): - return self.__str__() - - -class NeuState(ObjState): - """Neuron State Management. """ - - def __call__(self, size): - if isinstance(size, int): - size = (size,) - elif isinstance(size, (tuple, list)): - size = tuple(size) - else: - raise ValueError(f'Unknown size type: {type(size)}.') - - data = np.zeros((len(self._vars),) + size, dtype=np.float_) - var2idx = dict() - idx2var = dict() - state = dict() - for i, (k, v) in enumerate(self._vars.items()): - state[k] = data[i] - data[i] = v - var2idx[k] = i - idx2var[i] = k - state['_data'] = data - state['_data_cuda'] = None - state['_var2idx'] = var2idx - state['_idx2var'] = idx2var - - dict.__init__(self, state) - - return self - - def make_copy(self, size): - obj = NeuState(self._vars) - return obj(size=size) - - -@nb.njit([nb.types.UniTuple(nb.int64[:], 2)(nb.int64[:], nb.int64[:], nb.int64[:]), - nb.types.UniTuple(nb.int64, 2)(nb.int64, nb.int64, nb.int64)]) -def update_delay_indices(delay_in, delay_out, delay_len): - _delay_in = (delay_in + 1) % delay_len - _delay_out = (delay_out + 1) % delay_len - return _delay_in, _delay_out - - -class SynState(ObjState): - """Synapse State Management. """ - - def __init__(self, *args, help='', **kwargs): - super(SynState, self).__init__(*args, help=help, **kwargs) - self._delay_len = 1 - self._delay_in = 0 - self._delay_out = 0 - - def __call__(self, size, delay=None, delay_vars=()): - # check size - if isinstance(size, int): - size = (size,) - elif isinstance(size, (tuple, list)): - size = tuple(size) - else: - raise ValueError(f'Unknown size type: {type(size)}.') - - # check delay - delay = 0 if (delay is None) or (delay < 1) else delay - assert isinstance(delay, int), '"delay" must be a int to specify the delay length.' - self._delay_len = delay - self._delay_in = delay - 1 - - # check delay_vars - if isinstance(delay_vars, str): - delay_vars = (delay_vars,) - elif isinstance(delay_vars, (tuple, list)): - delay_vars = tuple(delay_vars) - else: - raise ValueError(f'Unknown delay_vars type: {type(delay_vars)}.') - - # initialize data - length = len(self._vars) + delay * len(delay_vars) - data = np.zeros((length,) + size, dtype=np.float_) - var2idx = dict() - idx2var = dict() - state = dict() - for i, (k, v) in enumerate(self._vars.items()): - data[i] = v - state[k] = data[i] - var2idx[k] = i - idx2var[i] = k - index_offset = len(self._vars) - for i, v in enumerate(delay_vars): - var2idx[f'_{v}_offset'] = i * delay + index_offset - state[f'_{v}_delay'] = data[i * delay + index_offset: (i + 1) * delay + index_offset] - state['_data'] = data - state['_data_cuda'] = None - state['_var2idx'] = var2idx - state['_idx2var'] = idx2var - - dict.__init__(self, state) - - return self - - def make_copy(self, size, delay=None, delay_vars=()): - obj = SynState(self._vars) - return obj(size=size, delay=delay, delay_vars=delay_vars) - - def delay_push(self, g, var): - if self._delay_len > 0: - data = self.__getitem__('_data') - offset = self.__getitem__('_var2idx')[f'_{var}_offset'] - data[self._delay_in + offset] = g - - def delay_pull(self, var): - if self._delay_len > 0: - data = self.__getitem__('_data') - offset = self.__getitem__('_var2idx')[f'_{var}_offset'] - return data[self._delay_out + offset] - else: - data = self.__getitem__('_data') - var2idx = self.__getitem__('_var2idx') - return data[var2idx[var]] - - def _update_delay_indices(self): - din, dout = update_delay_indices(self._delay_in, self._delay_out, self._delay_len) - self._delay_in = din - self._delay_out = dout - - -class ListConn(TypeChecker): - """Synaptic connection with list type.""" - - def __init__(self, help=''): - super(ListConn, self).__init__(help=help) - - def check(self, cls): - if profile.is_jit(): - if not isinstance(cls, nb.typed.List): - raise errors.TypeMismatchError(f'In numba mode, "cls" must be an instance of {type(nb.typed.List)}, ' - f'but got {type(cls)}. Hint: you can use "ListConn.create()" method.') - if not isinstance(cls[0], (nb.typed.List, np.ndarray)): - raise errors.TypeMismatchError(f'In numba mode, elements in "cls" must be an instance of ' - f'{type(nb.typed.List)} or ndarray, but got {type(cls[0])}. ' - f'Hint: you can use "ListConn.create()" method.') - else: - if not isinstance(cls, list): - raise errors.TypeMismatchError(f'ListConn requires a list, but got {type(cls)}.') - if not isinstance(cls[0], (list, np.ndarray)): - raise errors.TypeMismatchError(f'ListConn requires the elements of the list must be list or ' - f'ndarray, but got {type(cls)}.') - - @classmethod - def make_copy(cls, conn): - assert isinstance(conn, (list, tuple)), '"conn" must be a tuple/list.' - assert isinstance(conn[0], (list, tuple)), 'Elements of "conn" must be tuple/list.' - if profile.is_jit(): - a_list = nb.typed.List() - for l in conn: - a_list.append(np.uint64(l)) - else: - a_list = conn - return a_list - - def __str__(self): - return 'ListConn' - - -class MatConn(TypeChecker): - """Synaptic connection with matrix (2d array) type.""" - - def __init__(self, help=''): - super(MatConn, self).__init__(help=help) - - def check(self, cls): - if not (isinstance(cls, np.ndarray) and np.ndim(cls) == 2): - raise errors.TypeMismatchError(f'MatConn requires a two-dimensional ndarray.') - - def __str__(self): - return 'MatConn' - - -class SliceConn(TypeChecker): - def __init__(self, help=''): - super(SliceConn, self).__init__(help=help) - - def check(self, cls): - if not (isinstance(cls, np.ndarray) and np.shape[1] == 2): - raise errors.TypeMismatchError(f'') - - def __str__(self): - return 'SliceConn' - - -class Array(TypeChecker): - """NumPy ndarray.""" - - def __init__(self, dim, help=''): - self.dim = dim - super(Array, self).__init__(help=help) - - def __call__(self, size): - if isinstance(size, int): - assert self.dim == 1 - else: - assert len(size) == self.dim - return np.zeros(size, dtype=np.float_) - - def check(self, cls): - if not (isinstance(cls, np.ndarray) and np.ndim(cls) == self.dim): - raise errors.TypeMismatchError(f'MatConn requires a {self.dim}-D ndarray.') - - def __str__(self): - return type(self).__name__ + f' (dim={self.dim})' - - -class String(TypeChecker): - def __init__(self, help=''): - super(String, self).__init__(help=help) - - def check(self, cls): - if not isinstance(cls, str): - raise errors.TypeMismatchError(f'Require a string, got {type(cls)}.') - - def __str__(self): - return 'StringType' - - -class Int(TypeChecker): - def __init__(self, help=''): - super(Int, self).__init__(help=help) - - def check(self, cls): - if not isinstance(cls, int): - raise errors.TypeMismatchError(f'Require an int, got {type(cls)}.') - - def __str__(self): - return 'IntType' - - -class Float(TypeChecker): - def __init__(self, help=''): - super(Float, self).__init__(help=help) - - def check(self, cls): - if not isinstance(cls, float): - raise errors.TypeMismatchError(f'Require a float, got {type(cls)}.') - - def __str__(self): - return 'Floatype' - - -class List(TypeChecker): - def __init__(self, item_type=None, help=''): - if item_type is None: - self.item_type = None - else: - assert isinstance(item_type, TypeChecker), 'Must be a TypeChecker.' - self.item_type = item_type - - super(List, self).__init__(help=help) - - def check(self, cls): - if profile.is_jit(): - if not isinstance(cls, nb.typed.List): - raise errors.TypeMismatchError(f'In numba, "List" requires an instance of {type(nb.typed.List)}, ' - f'but got {type(cls)}.') - else: - if not isinstance(cls, list): - raise errors.TypeMismatchError(f'"List" requires an instance of list, ' - f'but got {type(cls)}.') - - if self.item_type is not None: - self.item_type.check(cls[0]) - - def __str__(self): - return type(self).__name__ + f'(item_type={str(self.item_type)})' - - -class Dict(TypeChecker): - def __init__(self, key_type=String, item_type=None, help=''): - if key_type is not None: - assert isinstance(key_type, TypeChecker), 'Must be a TypeChecker.' - self.key_type = key_type - if item_type is not None: - assert isinstance(item_type, TypeChecker), 'Must be a TypeChecker.' - self.item_type = item_type - super(Dict, self).__init__(help=help) - - def check(self, cls): - if profile.is_jit(): - if not isinstance(cls, nb.typed.Dict): - raise errors.TypeMismatchError(f'In numba, "Dict" requires an instance of {type(nb.typed.Dict)}, ' - f'but got {type(cls)}.') - else: - if not isinstance(cls, dict): - raise errors.TypeMismatchError(f'"Dict" requires an instance of dict, ' - f'but got {type(cls)}.') - - if self.key_type is not None: - for key in cls.keys(): - self.key_type.check(key) - if self.item_type is not None: - for item in cls.items(): - self.item_type.check(item) - - def __str__(self): - return type(self).__name__ + f'(key_type={str(self.key_type)}, item_type={str(self.item_type)})' diff --git a/brainpy/core/utils.py b/brainpy/core/utils.py deleted file mode 100644 index 75f0ee17..00000000 --- a/brainpy/core/utils.py +++ /dev/null @@ -1,151 +0,0 @@ -# -*- coding: utf-8 -*- - -import inspect -from pprint import pprint - -from numba.core.dispatcher import Dispatcher - -from .. import backend -from .. import errors -from .. import integration -from .. import tools - -__all__ = [ - 'show_code_str', - 'show_code_scope', - 'find_integrators', - 'get_func_scope', - 'check_slice', -] - - -def check_slice(start, end, length): - if start >= end: - raise errors.ModelUseError(f'Illegal start/end values for subgroup, {start}>={end}') - if start >= length: - raise errors.ModelUseError(f'Illegal start value for subgroup, {start}>={length}') - if end > length: - raise errors.ModelUseError(f'Illegal stop value for subgroup, {end}>{length}') - if start < 0: - raise errors.ModelUseError('Indices have to be positive.') - - -def show_code_str(func_code): - print(func_code) - print() - - -def show_code_scope(code_scope, ignores=()): - scope = {} - for k, v in code_scope.items(): - if k in ignores: - continue - if k in integration.CONSTANT_MAPPING: - continue - if k in integration.FUNCTION_MAPPING: - continue - scope[k] = v - pprint(scope) - print() - - -def find_integrators(func): - """Find integrators in a given function. - - Parameters - ---------- - func : callable - The function. - - Returns - ------- - integrators : list - A list of integrators. - """ - if not callable(func) or type(func).__name__ != 'function': - return [] - - integrals = [] - variables = inspect.getclosurevars(func) - scope = dict(variables.nonlocals) - scope.update(variables.globals) - for val in scope.values(): - if isinstance(val, integration.Integrator): - integrals.append(val) - elif callable(val): - integrals.extend(find_integrators(val)) - return integrals - - -def _update_scope(k, v, scope): - if type(v).__name__ in ['module', 'function']: - return - if isinstance(v, integration.Integrator): - return - if k in scope: - if v != scope[k]: - raise ValueError(f'Find scope variable {k} have different values: \n' - f'{k} = {v} and {k} = {scope[k]}. \n' - f'This maybe cause a grievous mistake in the future. Please change!') - scope[k] = v - - -def get_func_scope(func, include_dispatcher=False): - """Get function scope variables. - - Parameters - ---------- - func : callable, Integrator - include_dispatcher - - Returns - ------- - - """ - # get function scope - if isinstance(func, integration.Integrator): - func_name = func.py_func_name - variables = inspect.getclosurevars(func.diff_eq.func) - scope = dict(variables.nonlocals) - scope.update(variables.globals) - elif type(func).__name__ == 'function': - func_name = tools.get_func_name(func, replace=True) - variables = inspect.getclosurevars(func) - if func_name.startswith('xoroshiro128p_'): - return {} - scope = dict(variables.nonlocals) - scope.update(variables.globals) - else: - if backend.func_in_numpy_or_math(func): - return {} - elif isinstance(func, Dispatcher) and include_dispatcher: - scope = get_func_scope(func.py_func) - else: - raise ValueError(f'Unknown type: {type(func)}') - - # update scope - for k, v in list(scope.items()): - # get the scope of the function item - if callable(v): - if isinstance(v, Dispatcher): - if include_dispatcher: - for k2, v2 in get_func_scope(v.py_func).items(): - try: - _update_scope(k2, v2, scope) - except ValueError: - raise ValueError(f'Definition error in function "{func_name}".') - else: - for k2, v2 in get_func_scope(v).items(): - try: - _update_scope(k2, v2, scope) - except ValueError: - raise ValueError(f'Definition error in function "{func_name}".') - - for k in list(scope.keys()): - v = scope[k] - if type(v).__name__ in ['module', 'function']: - scope.pop(k) - if isinstance(v, integration.Integrator): - scope.pop(k) - - return scope diff --git a/brainpy/errors.py b/brainpy/errors.py index 0b234d24..c9830f94 100644 --- a/brainpy/errors.py +++ b/brainpy/errors.py @@ -11,21 +11,18 @@ class ModelUseError(Exception): pass -class TypeMismatchError(Exception): +class DiffEqError(Exception): pass -class IntegratorError(Exception): +class CodeError(Exception): pass -class DiffEquationError(Exception): +class AnalyzerError(Exception): pass -class CodeError(Exception): +class PackageMissingError(Exception): pass - -class AnalyzerError(Exception): - pass diff --git a/brainpy/inputs.py b/brainpy/inputs.py index c47cca38..6490c63b 100644 --- a/brainpy/inputs.py +++ b/brainpy/inputs.py @@ -1,22 +1,13 @@ # -*- coding: utf-8 -*- import numpy as np -from numba.cuda import random -from . import profile -from . import tools -from .core import NeuGroup -from .core import NeuType -from .core.types import NeuState -from .errors import ModelUseError +from brainpy import profile __all__ = [ 'constant_current', 'spike_current', 'ramp_current', - 'PoissonInput', - 'SpikeTimeInput', - 'FreqInput', ] @@ -143,258 +134,3 @@ def ramp_current(c_start, c_end, duration, t_start=0, t_end=None, dt=None): p2 = int(np.ceil(t_end / dt)) current[p1: p2] = np.linspace(c_start, c_end, p2 - p1) return current - - -class PoissonInput(NeuGroup): - """The Poisson input neuron group. - - Note: The ``PoissonGroup`` does not work for high-frequency rates. This is because - more than one spike might fall into a single time step (``dt``). - However, you can split high frequency rates into several neurons with lower frequency rates. - For example, use ``PoissonGroup(10, 100)`` instead of ``PoissonGroup(1, 1000)``. - - Parameters - ---------- - geometry : int, tuple, list - The neuron group geometry. - freqs : float, int, np.ndarray - The spike rates. - monitors : list, tuple - The targets for monitoring. - name : str - The neuron group name. - """ - - def __init__(self, geometry, freqs, monitors=None, name=None): - dt = profile.get_dt() / 1000. - - # firing rate - if isinstance(freqs, np.ndarray): - freqs = freqs.flatten() - if not np.all(freqs <= 1000. / profile.get_dt()): - print(f'WARNING: The maximum supported frequency at dt={profile.get_dt()} ms ' - f'is {1000. / profile.get_dt()} Hz. While we get your "freq" setting which ' - f'is bigger than that.') - - # neuron model on CPU - # ------------------- - if profile.run_on_cpu(): - def update(ST): - ST['spike'] = np.random.random(ST['spike'].shape) < freqs * dt - - model = NeuType(name='poisson_input', ST=NeuState('spike'), steps=update, mode='vector') - - # neuron model on GPU - # ------------------- - else: - def update(ST, rng_states, _obj_i): - ST['spike'] = random.xoroshiro128p_uniform_float64(rng_states, _obj_i) < freqs * dt - - model = NeuType(name='poisson_input', ST=NeuState('spike'), steps=update, mode='scalar') - - # initialize neuron group - # ----------------------- - super(PoissonInput, self).__init__(model=model, geometry=geometry, monitors=monitors, name=name) - - # will automatically handle - # the heterogeneous problem - # ------------------------- - self.pars['freqs'] = freqs - - # rng states - # ---------- - if profile.run_on_gpu(): - num_block, num_thread = tools.get_cuda_size(self.num) - self.rng_states = random.create_xoroshiro128p_states( - num_block * num_thread, seed=np.random.randint(100000)) - - -class SpikeTimeInput(NeuGroup): - """The input neuron group characterized by spikes emitting at given times. - - >>> # Get 2 neurons, firing spikes at 10 ms and 20 ms. - >>> SpikeTimeInput(2, times=[10, 20]) - >>> # or - >>> # Get 2 neurons, the neuron 0 fires spikes at 10 ms and 20 ms. - >>> SpikeTimeInput(2, times=[10, 20], indices=[0, 0]) - >>> # or - >>> # Get 2 neurons, neuron 0 fires at 10 ms and 30 ms, neuron 1 fires at 20 ms. - >>> SpikeTimeInput(2, times=[10, 20, 30], indices=[0, 1, 0]) - >>> # or - >>> # Get 2 neurons; at 10 ms, neuron 0 fires; at 20 ms, neuron 0 and 1 fire; - >>> # at 30 ms, neuron 1 fires. - >>> SpikeTimeInput(2, times=[10, 20, 20, 30], indices=[0, 0, 1, 1]) - - Parameters - ---------- - geometry : int, tuple, list - The neuron group geometry. - indices : int, list, tuple - The neuron indices at each time point to emit spikes. - times : list, np.ndarray - The time points which generate the spikes. - monitors : list, tuple - The targets for monitoring. - name : str - The group name. - """ - - def __init__(self, geometry, times, indices=None, monitors=None, name=None, need_sort=True): - # number of neurons - # ----------------- - if isinstance(geometry, (int, float)): - num = int(geometry) - elif isinstance(geometry, (tuple, list)): - num = int(np.prod(geometry)) - else: - raise ModelUseError(f'"geometry" must be a int, or a tuple/list of int, ' - f'but we got {type(geometry)}.') - - # indices is not provided - # ----------------------- - if indices is None: - # data about times - times = np.ascontiguousarray(times, dtype=np.float_) - if need_sort: times = np.sort(times) - num_times = len(times) - - # model on CPU - if profile.run_on_cpu(): - def update(ST, _t, idx): - in_idx = idx[0] - if (in_idx < num_times) and (_t >= times[in_idx]): - ST['spike'] = 1. - idx += 1 - else: - ST['spike'] = 0. - - model = NeuType(name='time_input', ST=NeuState('spike'), - steps=update, mode='vector', - hand_overs={'idx': np.array([0])}) - - else: - def update(ST, _t, idxs, _obj_i): - in_idx = idxs[_obj_i] - if (in_idx < num_times) and (_t >= times[in_idx]): - ST['spike'] = 1. - idxs[_obj_i] += 1 - else: - ST['spike'] = 0. - - model = NeuType(name='time_input', ST=NeuState('spike'), - steps=update, mode='scalar', - hand_overs={'idxs': np.zeros(num, dtype=np.int_)}) - - # indices and times are provided - # ------------------------------ - - else: - if len(indices) != len(times): - raise ModelUseError(f'The length of "indices" and "times" must be the same. ' - f'However, we got {len(indices)} != {len(times)}.') - - if profile.run_on_cpu(): - - # data about times and indices - times = np.ascontiguousarray(times, dtype=np.float_) - indices = np.ascontiguousarray(indices, dtype=np.int_) - num_times = len(times) - if need_sort: - sort_idx = np.argsort(times) - indices = indices[sort_idx] - - # update logic - def update(ST, _t, idx): - ST['spike'] = 0. - while idx[0] < num_times and _t >= times[idx[0]]: - ST['spike'][indices[idx[0]]] = 1. - idx += 1 - - model = NeuType(name='time_input', ST=NeuState('spike'), - steps=update, mode='vector', - hand_overs={'idx': np.array([0])}) - - else: - raise NotImplementedError - - # neuron group - super(SpikeTimeInput, self).__init__(model=model, - geometry=geometry, - monitors=monitors, - name=name) - - -class FreqInput(NeuGroup): - """The input neuron group characterized by frequency. - - For examples: - - >>> # Get 2 neurons, with 10 Hz firing rate. - >>> FreqInput(2, freq=10.) - >>> # Get 4 neurons, with 20 Hz firing rate. The neurons - >>> # start firing at [10, 30] ms randomly. - >>> FreqInput(4, freq=20., start_time=np.random.randint(10, 30, (4,))) - - Parameters - ---------- - geometry : int, list, tuple - The geometry of neuron group. - freqs : int, float, np.ndarray - The output spike frequency. - start_time : float - The time of the first spike. - monitors : list, tuple - The targets for monitoring. - name : str - The name of the neuron group. - """ - - def __init__(self, geometry, freqs, start_time=0., monitors=None, name=None): - if not np.allclose(freqs <= 1000. / profile.get_dt()): - print(f'WARNING: The maximum supported frequency at dt={profile.get_dt()} ms ' - f'is {1000. / profile.get_dt()} Hz. While we get your "freq" setting which ' - f'is bigger than that.') - - state = NeuState({'spike': 0., 't_next_spike': 0., 't_last_spike': -1e7}) - - if profile.is_jit(): - def update_state(ST, _t_): - if _t_ >= ST['t_next_spike']: - ST['spike'] = 1. - ST['t_last_spike'] = _t_ - ST['t_next_spike'] += 1000. / freqs - else: - ST['spike'] = 0. - - model = NeuType(name='poisson_input', - ST=state, - steps=update_state, - mode='scalar') - - else: - if np.size(freqs) == 1: - def update_state(ST, _t_): - should_spike = _t_ >= ST['t_next_spike'] - ST['spike'] = should_spike - spike_ids = np.where(should_spike)[0] - ST['t_last_spike'][spike_ids] = _t_ - ST['t_next_spike'][spike_ids] += 1000. / freqs - - else: - def update_state(ST, _t_): - should_spike = _t_ >= ST['t_next_spike'] - ST['spike'] = should_spike - spike_ids = np.where(should_spike)[0] - ST['t_last_spike'][spike_ids] = _t_ - ST['t_next_spike'][spike_ids] += 1000. / freqs[spike_ids] - - model = NeuType(name='freq_input', - ST=state, - steps=update_state, - mode='vector') - - # neuron group - super(FreqInput, self).__init__(model=model, geometry=geometry, monitors=monitors, name=name) - - self.ST['t_next_spike'] = start_time - self.pars['freqs'] = freqs diff --git a/brainpy/integration/__init__.py b/brainpy/integration/__init__.py deleted file mode 100644 index bbf16926..00000000 --- a/brainpy/integration/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import diff_equation -from . import integrator -from . import utils -from .diff_equation import * -from .integrator import * -from .utils import * -from .. import profile - -_SUPPORT_METHODS = [ - 'euler', - 'midpoint', - 'heun', - 'rk2', - 'rk3', - 'rk4', - 'rk4_alternative', - 'exponential', - 'milstein', - 'milstein_ito', - 'milstein_stra', -] - - -def integrate(func=None, method=None): - """Generate the one-step integrator function for differential equations. - - Using this method, the users only need to define the right side of the equation. - For example, for the `m` channel in the Hodgkin–Huxley neuron model - - .. math:: - - \\alpha = {0.1 * (V + 40 \\over 1 - \\exp(-(V + 40) / 10)} - - \\beta = 4.0 * \\exp(-(V + 65) / 18) - - {dm \\over dt} = \\alpha * (1 - m) - \\beta * m - - Using ``BrainPy``, this ODE function can be written as - - >>> import numpy as np - >>> from brainpy import integrate - >>> - >>> @integrate(method='rk4') - >>> def int_m(m, t, V): - >>> alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) - >>> beta = 4.0 * np.exp(-(V + 65) / 18) - >>> return alpha * (1 - m) - beta * m - - Parameters - ---------- - func : callable - The function at the right hand of the differential equation. - If a stochastic equation (SDE) is defined, then `func` is the drift coefficient - (the deterministic part) of the SDE. - method : None, str, callable - The method of numerical integrator. - - Returns - ------- - integrator : Integrator - If `f` is provided, then the one-step numerical integrator will be returned. - if not, the wrapper will be provided. - """ - - method = method if method is not None else profile.get_numerical_method() - _integrator_ = get_integrator(method) - - if func is None: - return lambda f: _integrator_(DiffEquation(func=f)) - - else: - return _integrator_(DiffEquation(func=func)) diff --git a/brainpy/integration/constants.py b/brainpy/integration/constants.py deleted file mode 100644 index 82132a65..00000000 --- a/brainpy/integration/constants.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- - - -CONSTANT_NOISE = 'CONSTANT' -FUNCTIONAL_NOISE = 'FUNCTIONAL' - -ODE_TYPE = 'ODE' -SDE_TYPE = 'SDE' - -DIFF_EQUATION = 'diff_equation' -SUB_EXPRESSION = 'sub_expression' -RETURN_TYPES = [ - # return type # multi_return # DF type - 'x', # False # ODE - 'x,0', # False # ODE [x] - '(x,),', # False # ODE - '(x,),...', # True # ODE - '(x,0),', # False # ODE [x] - '(x,0),...', # True # ODE [x] - 'x,x', # False # SDE - '(x,x),', # False # SDE - '(x,x),...', # True # SDE -] - diff --git a/brainpy/integration/diff_equation.py b/brainpy/integration/diff_equation.py deleted file mode 100644 index 1ff322db..00000000 --- a/brainpy/integration/diff_equation.py +++ /dev/null @@ -1,335 +0,0 @@ -# -*- coding: utf-8 -*- - -import inspect -from collections import Counter - -import sympy - -from . import constants -from . import utils -from .. import errors -from .. import profile -from .. import tools - -__all__ = [ - 'Expression', - 'DiffEquation', -] - - -class Expression(object): - def __init__(self, var, code): - self.var_name = var - self.code = code.strip() - self._substituted_code = None - - @property - def identifiers(self): - return tools.get_identifiers(self.code) - - def __str__(self): - return f'{self.var_name} = {self.code}' - - def __repr__(self): - return self.__str__() - - def __eq__(self, other): - if not isinstance(other, Expression): - return NotImplemented - if self.code != other.code: - return False - if self.var_name != other.var_name: - return False - return True - - def __ne__(self, other): - return not self.__eq__(other) - - def get_code(self, subs=True): - if subs: - if self._substituted_code is None: - return self.code - else: - return self._substituted_code - else: - return self.code - - -class DiffEquation(object): - """Differential Equation. - - A differential equation is defined as the standard form: - - dx/dt = f(x) + g(x) dW - - Parameters - ---------- - func : callable - The user defined differential equation. - """ - - def __init__(self, func): - # check - if func is None: - raise errors.DiffEquationError('"func" cannot be None.') - if not (callable(func) and type(func).__name__ == 'function'): - raise errors.DiffEquationError('"func" must be a function.') - - # function - self.func = func - - # function string - self.code = tools.deindent(tools.get_main_code(func)) - if 'return' not in self.code: - raise errors.DiffEquationError(f'"func" function must return something, ' - f'but found no return.\n{self.code}') - - # function arguments - self.func_args = inspect.getfullargspec(func).args - - # function name - if tools.is_lambda_function(func): - self.func_name = f'_integral_{self.func_args[0]}_' - else: - self.func_name = func.__name__ - - # function scope - scope = inspect.getclosurevars(func) - self.func_scope = dict(scope.nonlocals) - self.func_scope.update(scope.globals) - - # differential variable name and time name - self.var_name = self.func_args[0] - self.t_name = self.func_args[1] - - # analyse function code - res = utils.analyse_diff_eq(self.code) - self.expressions = [Expression(v, expr) for v, expr in zip(res.variables, res.expressions)] - self.return_intermediates = res.return_intermediates - self.return_type = res.return_type - self.f_expr = None - self.g_expr = None - if res.f_expr is not None: - self.f_expr = Expression(res.f_expr[0], res.f_expr[1]) - if res.g_expr is not None: - self.g_expr = Expression(res.g_expr[0], res.g_expr[1]) - for k, num in Counter(res.variables).items(): - if num > 1: - raise errors.DiffEquationError( - f'Found "{k}" {num} times. Please assign each expression ' - f'in differential function with a unique name. ') - - # analyse noise type - self.g_type = constants.CONSTANT_NOISE - self.g_value = None - if self.g_expr is not None: - self._substitute(self.g_expr, self.expressions) - g_code = self.g_expr.get_code(subs=True) - for idf in tools.get_identifiers(g_code): - if idf not in self.func_scope: - self.g_type = constants.FUNCTIONAL_NOISE - break - else: - self.g_value = eval(g_code, self.func_scope) - - def _substitute(self, final_exp, expressions, substitute_vars=None): - """Substitute expressions to get the final single expression - - Parameters - ---------- - final_exp : Expression - The final expression. - expressions : list, tuple - The list/tuple of expressions. - """ - if substitute_vars is None: - return - if final_exp is None: - return - assert substitute_vars == 'all' or \ - substitute_vars == self.var_name or \ - isinstance(substitute_vars, (tuple, list)) - - # Goal: Substitute dependent variables into the expresion - # Hint: This step doesn't require the left variables are unique - dependencies = {} - for expr in expressions: - substitutions = {} - for dep_var, dep_expr in dependencies.items(): - if dep_var in expr.identifiers: - code = dep_expr.get_code(subs=True) - substitutions[sympy.Symbol(dep_var, real=True)] = utils.str2sympy(code).expr - if len(substitutions): - new_sympy_expr = utils.str2sympy(expr.code).expr.xreplace(substitutions) - new_str_expr = utils.sympy2str(new_sympy_expr) - expr._substituted_code = new_str_expr - dependencies[expr.var_name] = expr - else: - if substitute_vars == 'all': - dependencies[expr.var_name] = expr - elif substitute_vars == self.var_name: - if self.var_name in expr.identifiers: - dependencies[expr.var_name] = expr - else: - ids = expr.identifiers - for var in substitute_vars: - if var in ids: - dependencies[expr.var_name] = expr - break - - # Goal: get the final differential equation - # Hint: the step requires the expression variables must be unique - substitutions = {} - for dep_var, dep_expr in dependencies.items(): - code = dep_expr.get_code(subs=True) - substitutions[sympy.Symbol(dep_var, real=True)] = utils.str2sympy(code).expr - if len(substitutions): - new_sympy_expr = utils.str2sympy(final_exp.code).expr.xreplace(substitutions) - new_str_expr = utils.sympy2str(new_sympy_expr) - final_exp._substituted_code = new_str_expr - - def get_f_expressions(self, substitute_vars=None): - if self.f_expr is None: - return [] - self._substitute(self.f_expr, self.expressions, substitute_vars=substitute_vars) - - return_expressions = [] - # the derivative expression - dif_eq_code = self.f_expr.get_code(subs=True) - return_expressions.append(Expression(f'_df{self.var_name}_dt', dif_eq_code)) - # needed variables - need_vars = tools.get_identifiers(dif_eq_code) - need_vars |= tools.get_identifiers(', '.join(self.return_intermediates)) - # get the total return expressions - for expr in self.expressions[::-1]: - if expr.var_name in need_vars: - if not profile._substitute_equation or expr._substituted_code is None: - code = expr.code - else: - code = expr._substituted_code - return_expressions.append(Expression(expr.var_name, code)) - need_vars |= tools.get_identifiers(code) - return return_expressions[::-1] - - def get_g_expressions(self): - if self.g_expr is None: - return [] - - if self.is_functional_noise: - return_expressions = [] - # the derivative expression - eq_code = self.g_expr.get_code(subs=True) - return_expressions.append(Expression(f'_dg{self.var_name}_dt', eq_code)) - # needed variables - need_vars = tools.get_identifiers(eq_code) - # get the total return expressions - for expr in self.expressions[::-1]: - if expr.var_name in need_vars: - if not profile._substitute_equation or expr._substituted_code is None: - code = expr.code - else: - code = expr._substituted_code - return_expressions.append(Expression(expr.var_name, code)) - need_vars |= tools.get_identifiers(code) - return return_expressions[::-1] - else: - return [Expression(f'_dg{self.var_name}_dt', self.g_expr.get_code(subs=True))] - - def _replace_expressions(self, expressions, name, y_sub, t_sub=None): - """Replace expressions of df part. - - Parameters - ---------- - expressions : list, tuple - The list/tuple of expressions. - name : str - The name of the new expression. - y_sub : str - The new name of the variable "y". - t_sub : str, optional - The new name of the variable "t". - - Returns - ------- - list_of_expr : list - A list of expressions. - """ - return_expressions = [] - - # replacements - replacement = {self.var_name: y_sub} - if t_sub is not None: - replacement[self.t_name] = t_sub - - # replace variables in expressions - for expr in expressions: - replace = False - identifiers = expr.identifiers - for repl_var in replacement.keys(): - if repl_var in identifiers: - replace = True - break - if replace: - code = tools.word_replace(expr.code, replacement) - new_expr = Expression(f"{expr.var_name}_{name}", code) - return_expressions.append(new_expr) - replacement[expr.var_name] = new_expr.var_name - return return_expressions - - def replace_f_expressions(self, name, y_sub, t_sub=None): - """Replace expressions of df part. - - Parameters - ---------- - name : str - The name of the new expression. - y_sub : str - The new name of the variable "y". - t_sub : str, optional - The new name of the variable "t". - - Returns - ------- - list_of_expr : list - A list of expressions. - """ - return self._replace_expressions(self.get_f_expressions(), - name=name, - y_sub=y_sub, - t_sub=t_sub) - - def replace_g_expressions(self, name, y_sub, t_sub=None): - if self.is_functional_noise: - return self._replace_expressions(self.get_g_expressions(), - name=name, - y_sub=y_sub, - t_sub=t_sub) - else: - return [] - - @property - def is_stochastic(self): - if self.g_expr is not None: - try: - if eval(self.g_expr.code, self.func_scope) == 0.: - return False - except Exception as e: - pass - return True - else: - return False - - @property - def is_functional_noise(self): - return self.g_type == constants.FUNCTIONAL_NOISE - - @property - def stochastic_type(self): - if not self.is_stochastic: - return None - else: - pass - - @property - def expr_names(self): - return [expr.var_name for expr in self.expressions] diff --git a/brainpy/integration/integrator.py b/brainpy/integration/integrator.py deleted file mode 100644 index 2525e78f..00000000 --- a/brainpy/integration/integrator.py +++ /dev/null @@ -1,1109 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np -import sympy - -from . import diff_equation -from . import utils -from .. import backend -from .. import errors -from .. import profile -from .. import tools - -__all__ = [ - 'get_integrator', - 'Integrator', - 'Euler', - 'Heun', - 'MidPoint', - 'RK2', - 'RK3', - 'RK4', - 'RK4Alternative', - 'ExponentialEuler', - 'MilsteinIto', - 'MilsteinStra', -] - - -def get_integrator(method): - method = method.lower() - - if method == 'euler': - return Euler - elif method == 'midpoint': - return MidPoint - elif method == 'heun': - return Heun - elif method == 'rk2': - return RK2 - elif method == 'rk3': - return RK3 - elif method == 'rk4': - return RK4 - elif method == 'rk4_alternative': - return RK4Alternative - elif method == 'exponential': - return ExponentialEuler - elif method == 'milstein': - return MilsteinIto - elif method == 'milstein_ito': - return MilsteinIto - elif method == 'milstein_stra': - return MilsteinStra - else: - raise ValueError(f'Unknown method: {method}.') - - -class Integrator(object): - def __init__(self, diff_eq): - if not isinstance(diff_eq, diff_equation.DiffEquation): - if diff_eq.__class__.__name__ != 'function': - raise errors.IntegratorError('"diff_eq" must be a function or an instance of DiffEquation .') - else: - diff_eq = diff_equation.DiffEquation(func=diff_eq) - self.diff_eq = diff_eq - self._update_code = None - self._update_func = None - - def __call__(self, y0, t, *args): - return self._update_func(y0, t, *args) - - def _compile(self): - # function arguments - func_args = ', '.join([f'_{arg}' for arg in self.diff_eq.func_args]) - - # function codes - func_code = f'def {self.py_func_name}({func_args}): \n' - func_code += tools.indent(self._update_code + '\n' + f'return _res') - tools.NoiseHandler.normal_pattern.sub( - tools.NoiseHandler.vector_replace_f, func_code) - - # function scope - code_scopes = {'numpy': np} - for k_, v_ in self.code_scope.items(): - if profile.is_jit() and callable(v_): - v_ = tools.numba_func(v_) - code_scopes[k_] = v_ - code_scopes.update(utils.get_mapping_scope()) - code_scopes['_normal_like_'] = backend.normal_like - - # function compilation - exec(compile(func_code, '', 'exec'), code_scopes) - func = code_scopes[self.py_func_name] - if profile.is_jit(): - func = tools.jit(func) - self._update_func = func - - @staticmethod - def get_integral_step(diff_eq, *args): - raise NotImplementedError - - @property - def py_func_name(self): - return self.diff_eq.func_name - - @property - def update_code(self): - return self._update_code - - @property - def update_func(self): - return self._update_func - - @property - def code_scope(self): - scope = self.diff_eq.func_scope - if profile.run_on_cpu(): - scope['_normal_like_'] = backend.normal_like - return scope - - -class Euler(Integrator): - """Forward Euler method. Also named as ``explicit_Euler``. - - The simplest way for solving ordinary differential equations is "the - Euler method" by Press et al. (1992) [1]_ : - - .. math:: - - y_{n+1} = y_n + f(y_n, t_n) \\Delta t - - This formula advances a solution from :math:`y_n` to :math:`y_{n+1}=y_n+h`. - Note that the method increments a solution through an interval :math:`h` - while using derivative information from only the beginning of the interval. - As a result, the step's error is :math:`O(h^2)`. - - For SDE equations, this approximation is a continuous time stochastic process that - satisfy the iterative scheme [1]_. - - .. math:: - - Y_{n+1} = Y_n + f(Y_n)h_n + g(Y_n)\\Delta W_n - - where :math:`n=0,1, \\cdots , N-1`, :math:`Y_0=x_0`, :math:`Y_n = Y(t_n)`, - :math:`h_n = t_{n+1} - t_n` is the step size, - :math:`\\Delta W_n = [W(t_{n+1}) - W(t_n)] \\sim N(0, h_n)=\\sqrt{h}N(0, 1)` - with :math:`W(t_0) = 0`. - - For simplicity, we rewrite the above equation into - - .. math:: - - Y_{n+1} = Y_n + f_n h + g_n \\Delta W_n - - As the order of convergence for the Euler-Maruyama method is low (strong order of - convergence 0.5, weak order of convergence 1), the numerical results are inaccurate - unless a small step size is used. By adding one more term from the stochastic - Taylor expansion, one obtains a 1.0 strong order of convergence scheme known - as *Milstein scheme* [2]_. - - Parameters - ---------- - diff_eq : DiffEquation, callable - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - .. [1] W. H.; Flannery, B. P.; Teukolsky, S. A.; and Vetterling, - W. T. Numerical Recipes in FORTRAN: The Art of Scientific - Computing, 2nd ed. Cambridge, England: Cambridge University - Press, p. 710, 1992. - .. [2] U. Picchini, Sde toolbox: Simulation and estimation of stochastic - differential equations with matlab. - """ - - def __init__(self, diff_eq): - super(Euler, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - dt = profile.get_dt() - var_name = diff_eq.var_name - var = sympy.Symbol(var_name, real=True) - - # get code lines of df part - f_expressions = diff_eq.get_f_expressions() - code_lines = [str(expr) for expr in f_expressions] - dfdt = sympy.Symbol(f'_df{var_name}_dt') - - # get code lines of dg part - if diff_eq.is_stochastic: - noise = f'_normal_like_({var_name})' - code_lines.append(f'_{var_name}_dW = {noise}') - code_lines.extend([str(expr) for expr in diff_eq.get_g_expressions()]) - dgdt = sympy.Symbol(f'_{var_name}_dW') * sympy.Symbol(f'_dg{var_name}_dt') - else: - dgdt = 0 - - # update expression - update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {utils.sympy2str(update)}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - -class RK2(Integrator): - """Parametric second-order Runge-Kutta (RK2). Also named as ``RK2``. - - It is given in parametric form by [3]_ . - - .. math:: - - k_1 &= f(y_n, t_n) \\\\ - k_2 &= f(y_n + \\beta \\Delta t k_1, t_n + \\beta \\Delta t) \\\\ - y_{n+1} &= y_n + \\Delta t [(1-\\frac{1}{2\\beta})k_1+\\frac{1}{2\\beta}k_2] - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - beta : float - Popular choices for 'beta': - 1/2 : explicit midpoint method - 2/3 : Ralston's method - 1 : Heun's method, also known as the explicit trapezoid rule - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - .. [3] https://lpsa.swarthmore.edu/NumInt/NumIntSecond.html - - See Also - -------- - Heun, MidPoint - """ - - def __init__(self, diff_eq, beta=2 / 3): - super(RK2, self).__init__(diff_eq) - self.beta = beta - self._update_code = self.get_integral_step(diff_eq, beta) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, beta=2 / 3): - dt = profile.get_dt() - t_name = diff_eq.t_name - var_name = diff_eq.var_name - var = sympy.Symbol(var_name, real=True) - - # get code lines of k1 df part - k1_expressions = diff_eq.get_f_expressions(substitute_vars=None) - code_lines = [str(expr) for expr in k1_expressions[:-1]] - code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') - - # k1 -> k2 increment - y_1_to_2 = f'_{var_name}_k1_to_k2' - t_1_to_2 = f'_t_k1_to_k2' - code_lines.append(f'{y_1_to_2} = {var_name} + {beta * dt} * _df{var_name}_dt_k1') - code_lines.append(f'{t_1_to_2} = {t_name} + {beta * dt}') - - # get code lines of k2 df part - k2_expressions = diff_eq.replace_f_expressions('k2', y_sub=y_1_to_2, t_sub=t_1_to_2) - if len(k2_expressions): - code_lines.extend([str(expr) for expr in k2_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k2 = {k2_expressions[-1].code}') - - # final dt part - dfdt = sympy.Symbol(f'_df{var_name}_dt') - if len(k2_expressions): - coefficient2 = 1 / (2 * beta) - coefficient1 = 1 - coefficient2 - code_lines.append( - f'{dfdt.name} = {coefficient1} * _df{var_name}_dt_k1 + {coefficient2} * _df{var_name}_dt_k2') - else: - code_lines.append(f'{dfdt.name} = _df{var_name}_dt_k1') - - # get code lines of dg part - dgdt = 0 - if diff_eq.is_stochastic: - if not np.all(diff_eq.g_value == 0.): - raise NotImplementedError('RK2 currently doesn\'t support SDE.') - - # update expression - update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {utils.sympy2str(update)}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - -class Heun(Integrator): - """Two-stage method for numerical integrator. - - For ODE, please see "RK2". - - For stochastic Stratonovich integral, the Heun algorithm is given by, - according to paper [4]_ [5]_. - - .. math:: - Y_{n+1} &= Y_n + f_n h + {1 \\over 2}[g_n + g(\\overline{Y}_n)] \\Delta W_n - - \\overline{Y}_n &= Y_n + g_n \\Delta W_n - - - Or, it is written as - - .. math:: - - Y_1 &= y_n + f(y_n)h + g_n \\Delta W_n - - y_{n+1} &= y_n + {1 \over 2}[f(y_n) + f(Y_1)]h + {1 \\over 2} [g(y_n) + g(Y_1)] \\Delta W_n - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - .. [4] H. Gilsing and T. Shardlow, SDELab: A package for solving stochastic differential - equations in MATLAB, Journal of Computational and Applied Mathematics 205 (2007), - no. 2, 1002-1018. - .. [5] P.reversal_potential. Kloeden, reversal_potential. Platen, and H. Schurz, Numerical solution of SDE through computer - experiments, Springer, 1994. - - See Also - -------- - RK2, MidPoint, MilsteinStra - """ - - def __init__(self, diff_eq): - super(Heun, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - if diff_eq.is_stochastic: - if diff_eq.is_functional_noise: - dt = profile.get_dt() - var_name = diff_eq.var_name - var = sympy.Symbol(var_name, real=True) - - # k1 part # - # ------- # - - # df - f_k1_expressions = diff_eq.get_f_expressions(substitute_vars=None) - code_lines = [str(expr) for expr in f_k1_expressions[:-1]] - code_lines.append(f'_df{var_name}_dt_k1 = {f_k1_expressions[-1].code}') - - # dg - dW_sb = sympy.Symbol(f'_{var_name}_dW') - noise = f'_normal_like_({var_name})' - code_lines.append(f'{dW_sb.name} = sqrt({dt}) * {noise}') - g_k1_expressions = diff_eq.get_g_expressions() - code_lines.extend([str(expr) for expr in g_k1_expressions[:-1]]) - code_lines.append(f'_dg{var_name}_dt_k1 = {g_k1_expressions[-1].code}') - - # k1 - code_lines.append(f'_k1 = {var_name} + _df{var_name}_dt_k1 * {dt} + ' - f'_dg{var_name}_dt_k1 * {dW_sb.name}') - - # k2 part # - # ------- # - - # df - dfdt = sympy.Symbol(f'_df{var_name}_dt') - f_k2_expressions = diff_eq.replace_f_expressions('k2', y_sub='_k1') - if len(f_k2_expressions): - code_lines.extend([str(expr) for expr in f_k2_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k2 = {f_k2_expressions[-1].code}') - code_lines.append(f'{dfdt.name} = (_df{var_name}_dt_k1 + _df{var_name}_dt_k2) / 2') - else: - code_lines.append(f'{dfdt.name} = _df{var_name}_dt_k1') - - # dg - dgdt = sympy.Symbol(f'_dg{var_name}_dt') - g_k2_expressions = diff_eq.replace_f_expressions('k2', y_sub='_k1') - if len(g_k2_expressions): - code_lines.extend([str(expr) for expr in g_k2_expressions[:-1]]) - code_lines.append(f'_dg{var_name}_dt_k2 = {g_k2_expressions[-1].code}') - code_lines.append(f'{dgdt.name} = (_dg{var_name}_dt_k1 + _dg{var_name}_dt_k2) / 2') - else: - code_lines.append(f'{dgdt.name} = _dg{var_name}_dt_k1') - - # update expression - update = var + dfdt * dt + dgdt * dW_sb - code_lines.append(f'{var_name} = {utils.sympy2str(update)}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - else: - return Euler.get_integral_step(diff_eq) - else: - return RK2.get_integral_step(diff_eq, 1.0) - - -class MidPoint(Integrator): - """Explicit midpoint Euler method. Also named as ``modified_Euler``. - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - See Also - -------- - RK2, Heun - """ - - def __init__(self, diff_eq): - super(MidPoint, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - if diff_eq.is_stochastic: - raise NotImplementedError - else: - return RK2.get_integral_step(diff_eq, 0.5) - - -class RK3(Integrator): - """Kutta's third-order method (commonly known as RK3). - Also named as ``RK3`` [6]_ [7]_ [8]_ . - - .. math:: - - k_1 &= f(y_n, t_n) \\\\ - k_2 &= f(y_n + \\frac{\\Delta t}{2}k_1, tn+\\frac{\\Delta t}{2}) \\\\ - k_3 &= f(y_n -\\Delta t k_1 + 2\\Delta t k_2, t_n + \\Delta t) \\\\ - y_{n+1} &= y_{n} + \\frac{\\Delta t}{6}(k_1 + 4k_2+k_3) - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - .. [6] http://mathworld.wolfram.com/Runge-KuttaMethod.html - .. [7] https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods - .. [8] https://zh.wikipedia.org/wiki/龙格-库塔法 - - """ - - def __init__(self, diff_eq): - super(RK3, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - dt = profile.get_dt() - t_name = diff_eq.t_name - var_name = diff_eq.var_name - var = sympy.Symbol(var_name, real=True) - - # get code lines of k1 df part - k1_expressions = diff_eq.get_f_expressions(substitute_vars=None) - code_lines = [str(expr) for expr in k1_expressions[:-1]] - code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') - - # k1 -> k2 increment - y_1_to_2 = f'_{var_name}_k1_to_k2' - t_1_to_2 = f'_t_k1_to_k2' - code_lines.append(f'{y_1_to_2} = {var_name} + {dt / 2} * _df{var_name}_dt_k1') - code_lines.append(f'{t_1_to_2} = {t_name} + {dt / 2}') - - # get code lines of k2 df part - k2_expressions = diff_eq.replace_f_expressions('k2', y_sub=y_1_to_2, t_sub=t_1_to_2) - - dfdt = sympy.Symbol(f'_df{var_name}_dt') - if len(k2_expressions): - code_lines.extend([str(expr) for expr in k2_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k2 = {k2_expressions[-1].code}') - - # get code lines of k3 df part - y_1_to_3 = f'_{var_name}_k1_to_k3' - t_1_to_3 = f'_t_k1_to_k3' - code_lines.append(f'{y_1_to_3} = {var_name} - {dt} * _df{var_name}_dt_k1 + {2 * dt} * _df{var_name}_dt_k2') - code_lines.append(f'{t_1_to_3} = {t_name} + {dt}') - k3_expressions = diff_eq.replace_f_expressions('k3', y_sub=y_1_to_3, t_sub=t_1_to_3) - code_lines.extend([str(expr) for expr in k3_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k3 = {k3_expressions[-1].code}') - - # final df part - code_lines.append(f'{dfdt.name} = (_df{var_name}_dt_k1 + ' - f'4 * _df{var_name}_dt_k2 + _df{var_name}_dt_k3) / 6') - else: - # final df part - code_lines.append(f'{dfdt.name} = _df{var_name}_dt_k1') - - # get code lines of dg part - dgdt = 0 - if diff_eq.is_stochastic: - if not np.all(diff_eq.g_value == 0.): - raise NotImplementedError('RK3 currently doesn\'t support SDE.') - - # update expression - update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {utils.sympy2str(update)}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - -class RK4(Integrator): - """Fourth-order Runge-Kutta (RK4) [9]_ [10]_ [11]_ . - - .. math:: - - k_1 &= f(y_n, t_n) \\\\ - k_2 &= f(y_n + \\frac{\\Delta t}{2}k_1, t_n + \\frac{\\Delta t}{2}) \\\\ - k_3 &= f(y_n + \\frac{\\Delta t}{2}k_2, t_n + \\frac{\\Delta t}{2}) \\\\ - k_4 &= f(y_n + \\Delta t k_3, t_n + \\Delta t) \\\\ - y_{n+1} &= y_n + \\frac{\\Delta t}{6}(k_1 + 2*k_2 + 2* k_3 + k_4) - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - .. [9] http://mathworld.wolfram.com/Runge-KuttaMethod.html - .. [10] https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods - .. [11] https://zh.wikipedia.org/wiki/龙格-库塔法 - - """ - - def __init__(self, diff_eq): - super(RK4, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - dt = profile.get_dt() - t_name = diff_eq.t_name - var_name = diff_eq.var_name - var = sympy.Symbol(var_name, real=True) - - # get code lines of k1 df part - k1_expressions = diff_eq.get_f_expressions(substitute_vars=None) - code_lines = [str(expr) for expr in k1_expressions[:-1]] - code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') - - # k1 -> k2 increment - y_1_to_2 = f'_{var_name}_k1_to_k2' - t_1_to_2 = f'_t_k1_to_k2' - code_lines.append(f'{y_1_to_2} = {var_name} + {dt / 2} * _df{var_name}_dt_k1') - code_lines.append(f'{t_1_to_2} = {t_name} + {dt / 2}') - - # get code lines of k2 df part - k2_expressions = diff_eq.replace_f_expressions('k2', y_sub=y_1_to_2, t_sub=t_1_to_2) - - dfdt = sympy.Symbol(f'_df{var_name}_dt') - if len(k2_expressions): - code_lines.extend([str(expr) for expr in k2_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k2 = {k2_expressions[-1].code}') - - # get code lines of k3 df part - y_2_to_3 = f'_{var_name}_k2_to_k3' - t_2_to_3 = f'_t_k2_to_k3' - code_lines.append(f'{y_2_to_3} = {var_name} + {dt / 2} * _df{var_name}_dt_k2') - code_lines.append(f'{t_2_to_3} = {t_name} + {dt / 2}') - k3_expressions = diff_eq.replace_f_expressions('k3', y_sub=y_2_to_3, t_sub=t_2_to_3) - code_lines.extend([str(expr) for expr in k3_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k3 = {k3_expressions[-1].code}') - - # get code lines of k4 df part - y_3_to_4 = f'_{var_name}_k3_to_k4' - t_3_to_4 = f'_t_k3_to_k4' - code_lines.append(f'{y_3_to_4} = {var_name} + {dt} * _df{var_name}_dt_k3') - code_lines.append(f'{t_3_to_4} = {t_name} + {dt}') - k4_expressions = diff_eq.replace_f_expressions('k4', y_sub=y_3_to_4, t_sub=t_3_to_4) - code_lines.extend([str(expr) for expr in k4_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k4 = {k4_expressions[-1].code}') - - # final df part - code_lines.append(f'{dfdt.name} = (_df{var_name}_dt_k1 + 2 * _df{var_name}_dt_k2 + ' - f'2 * _df{var_name}_dt_k3 + _df{var_name}_dt_k4) / 6') - else: - # final df part - code_lines.append(f'{dfdt.name} = _df{var_name}_dt_k1') - - # get code lines of dg part - dgdt = 0 - if diff_eq.is_stochastic: - if not np.all(diff_eq.g_value == 0.): - raise NotImplementedError('RK4 currently doesn\'t support SDE.') - - # update expression - update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {utils.sympy2str(update)}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - -class RK4Alternative(Integrator): - """An alternative of fourth-order Runge-Kutta method. - Also named as ``RK4_alternative`` ("3/8" rule). - - It is a less often used fourth-order - explicit RK method, and was also proposed by Kutta [12]_: - - .. math:: - - k_1 &= f(y_n, t_n) \\\\ - k_2 &= f(y_n + \\frac{\\Delta t}{3}k_1, t_n + \\frac{\\Delta t}{3}) \\\\ - k_3 &= f(y_n - \\frac{\\Delta t}{3}k_1 + \\Delta t k_2, t_n + \\frac{2 \\Delta t}{3}) \\\\ - k_4 &= f(y_n + \\Delta t k_1 - \\Delta t k_2 + \\Delta t k_3, t_n + \\Delta t) \\\\ - y_{n+1} &= y_n + \\frac{\\Delta t}{8}(k_1 + 3*k_2 + 3* k_3 + k_4) - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - - .. [12] https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods - """ - - def __init__(self, diff_eq): - super(RK4Alternative, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - dt = profile.get_dt() - t_name = diff_eq.t_name - var_name = diff_eq.var_name - var = sympy.Symbol(var_name, real=True) - - # get code lines of k1 df part - k1_expressions = diff_eq.get_f_expressions(substitute_vars=None) - code_lines = [str(expr) for expr in k1_expressions[:-1]] - code_lines.append(f'_df{var_name}_dt_k1 = {k1_expressions[-1].code}') - - # k1 -> k2 increment - y_1_to_2 = f'_{var_name}_k1_to_k2' - t_1_to_2 = f'_t_k1_to_k2' - code_lines.append(f'{y_1_to_2} = {var_name} + {dt / 3} * _df{var_name}_dt_k1') - code_lines.append(f'{t_1_to_2} = {t_name} + {dt / 3}') - - # get code lines of k2 df part - k2_expressions = diff_eq.replace_f_expressions('k2', y_sub=y_1_to_2, t_sub=t_1_to_2) - - dfdt = sympy.Symbol(f'_df{var_name}_dt') - if len(k2_expressions): - code_lines.extend([str(expr) for expr in k2_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k2 = {k2_expressions[-1].code}') - - # get code lines of k3 df part - y_1_to_3 = f'_{var_name}_k1_to_k3' - t_1_to_3 = f'__t_k1_to_k3' - code_lines.append(f'{y_1_to_3} = {var_name} - {dt / 3} * _df{var_name}_dt_k1 + {dt} * _df{var_name}_dt_k2') - code_lines.append(f'{t_1_to_3} = {t_name} + {dt * 2 / 3}') - k3_expressions = diff_eq.replace_f_expressions('k3', y_sub=y_1_to_3, t_sub=t_1_to_3) - code_lines.extend([str(expr) for expr in k3_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k3 = {k3_expressions[-1].code}') - - # get code lines of k4 df part - y_1_to_4 = f'_{var_name}_k1_to_k4' - t_1_to_4 = f'_t_k1_to_k4' - code_lines.append(f'{y_1_to_4} = {var_name} + {dt} * _df{var_name}_dt_k1 - {dt} * _df{var_name}_dt_k2' - f'+ {dt} * _df{var_name}_dt_k3') - code_lines.append(f'{t_1_to_4} = {t_name} + {dt}') - k4_expressions = diff_eq.replace_f_expressions('k4', y_sub=y_1_to_4, t_sub=t_1_to_4) - code_lines.extend([str(expr) for expr in k4_expressions[:-1]]) - code_lines.append(f'_df{var_name}_dt_k4 = {k4_expressions[-1].code}') - - # final df part - code_lines.append(f'{dfdt.name} = (_df{var_name}_dt_k1 + 3 * _df{var_name}_dt_k2 + ' - f'3 * _df{var_name}_dt_k3 + _df{var_name}_dt_k4) / 8') - else: - # final df part - code_lines.append(f'{dfdt.name} = _df{var_name}_dt_k1') - - # get code lines of dg part - dgdt = 0 - if diff_eq.is_stochastic: - if not np.all(diff_eq.g_value == 0.): - raise NotImplementedError('RK4 currently doesn\'t support SDE.') - - # update expression - update = var + dfdt * dt + sympy.sqrt(dt) * dgdt - code_lines.append(f'{var_name} = {utils.sympy2str(update)}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - -class ExponentialEuler(Integrator): - """First order, explicit exponential Euler method. - - For an ODE equation of the form - - .. math:: - - y^{\\prime}=f(y), \quad y(0)=y_{0} - - its schema is given by - - .. math:: - - y_{n+1}= y_{n}+h \\varphi(hA) f (y_{n}) - - where :math:`A=f^{\prime}(y_{n})` and :math:`\\varphi(z)=\\frac{e^{z}-1}{z}`. - - For linear ODE system: :math:`y^{\\prime} = Ay + B`, - the above equation is equal to - - .. math:: - - y_{n+1}= y_{n}e^{hA}-B/A(1-e^{hA}) - - For a SDE equation of the form - - .. math:: - - d y=(Ay+ F(y))dt + g(y)dW(t) = f(y)dt + g(y)dW(t), \\quad y(0)=y_{0} - - its schema is given by [16]_ - - .. math:: - - y_{n+1} & =e^{\\Delta t A}(y_{n}+ g(y_n)\\Delta W_{n})+\\varphi(\\Delta t A) F(y_{n}) \\Delta t \\\\ - &= y_n + \\Delta t \\varphi(\\Delta t A) f(y) + e^{\\Delta t A}g(y_n)\\Delta W_{n} - - where :math:`\\varphi(z)=\\frac{e^{z}-1}{z}`. - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - .. [16] Erdoğan, Utku, and Gabriel J. Lord. "A new class of exponential integrators for stochastic - differential equations with multiplicative noise." arXiv preprint arXiv:1608.07096 (2016). - """ - - def __init__(self, diff_eq): - super(ExponentialEuler, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - dt = profile.get_dt() - f_expressions = diff_eq.get_f_expressions(substitute_vars=diff_eq.var_name) - - # code lines - code_lines = [str(expr) for expr in f_expressions[:-1]] - - # get the linear system using sympy - f_res = f_expressions[-1] - df_expr = utils.str2sympy(f_res.code).expr.expand() - s_df = sympy.Symbol(f"{f_res.var_name}") - code_lines.append(f'{s_df.name} = {utils.sympy2str(df_expr)}') - var = sympy.Symbol(diff_eq.var_name, real=True) - - # get df part - s_linear = sympy.Symbol(f'_{diff_eq.var_name}_linear') - s_linear_exp = sympy.Symbol(f'_{diff_eq.var_name}_linear_exp') - s_df_part = sympy.Symbol(f'_{diff_eq.var_name}_df_part') - if df_expr.has(var): - # linear - linear = sympy.collect(df_expr, var, evaluate=False)[var] - code_lines.append(f'{s_linear.name} = {utils.sympy2str(linear)}') - # linear exponential - linear_exp = sympy.exp(linear * dt) - code_lines.append(f'{s_linear_exp.name} = {utils.sympy2str(linear_exp)}') - # df part - df_part = (s_linear_exp - 1) / s_linear * s_df - code_lines.append(f'{s_df_part.name} = {utils.sympy2str(df_part)}') - - else: - # linear exponential - code_lines.append(f'{s_linear_exp.name} = sqrt({dt})') - # df part - code_lines.append(f'{s_df_part.name} = {utils.sympy2str(dt * s_df)}') - - # get dg part - if diff_eq.is_stochastic: - # dW - noise = f'_normal_like_({diff_eq.var_name})' - code_lines.append(f'_{diff_eq.var_name}_dW = {noise}') - # expressions of the stochastic part - g_expressions = diff_eq.get_g_expressions() - code_lines.extend([str(expr) for expr in g_expressions[:-1]]) - g_expr = g_expressions[-1].code - # get the dg_part - s_dg_part = sympy.Symbol(f'_{diff_eq.var_name}_dg_part') - code_lines.append(f'_{diff_eq.var_name}_dg_part = {g_expr} * _{diff_eq.var_name}_dW') - else: - s_dg_part = 0 - - # update expression - update = var + s_df_part + s_dg_part * s_linear_exp - - # The actual update step - code_lines.append(f'{diff_eq.var_name} = {utils.sympy2str(update)}') - return_expr = ', '.join([diff_eq.var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - -class MilsteinIto(Integrator): - """Itô stochastic integral. The derivative-free Milstein method is - an order 1.0 strong Taylor schema. - - The following implementation approximates this derivative thanks to a - Runge-Kutta approach [13]_. - - In Itô scheme, it is expressed as - - .. math:: - - Y_{n+1} = Y_n + f_n h + g_n \\Delta W_n + {1 \\over 2\\sqrt{h}} - [g(\\overline{Y_n}) - g_n] [(\\Delta W_n)^2-h] - - where :math:`\\overline{Y_n} = Y_n + f_n h + g_n \\sqrt{h}`. - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - .. [13] P.reversal_potential. Kloeden, reversal_potential. Platen, and H. Schurz, Numerical solution of SDE - through computer experiments, Springer, 1994. - - """ - - def __init__(self, diff_eq): - super(MilsteinIto, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - if diff_eq.is_stochastic: - if diff_eq.is_functional_noise: - g_dependent_on_var = diff_eq.replace_f_expressions('test', y_sub=f'test') - if len(g_dependent_on_var) == 0: - return Euler.get_integral_step(diff_eq) - - dt = profile.get_dt() - var_name = diff_eq.var_name - - # k1 part # - # ------- # - - # df - f_k1_expressions = diff_eq.get_f_expressions(substitute_vars=None) - code_lines = [str(expr) for expr in f_k1_expressions] # _df{var_name}_dt - - # dg - dW_sb = sympy.Symbol(f'_{var_name}_dW') - noise = f'_normal_like_({var_name})' - code_lines.append(f'{dW_sb.name} = sqrt({dt}) * {noise}') - g_k1_expressions = diff_eq.get_g_expressions() - code_lines.extend([str(expr) for expr in g_k1_expressions]) # _dg{var_name}_dt - - # high order part # - # --------------- # - k1_expr = f'_k1 = {var_name} + _df{var_name}_dt * {dt} + ' \ - f'_dg{var_name}_dt * sqrt({dt})' - high_order = sympy.Symbol(f'_dg{var_name}_high_order') - g_k2_expressions = diff_eq.replace_g_expressions('k2', y_sub=f'_k1') - - # dg high order - if len(g_k2_expressions): - code_lines.append(k1_expr) - code_lines.extend([str(expr) for expr in g_k2_expressions[:-1]]) - code_lines.append(f'_dg{var_name}_dt_k2 = {g_k2_expressions[-1].code}') - code_lines.append(f'{high_order.name} = 0.5 / sqrt({dt}) * ' - f'(_dg{var_name}_dt_k2 - _dg{var_name}_dt) *' - f'({dW_sb.name} * {dW_sb.name} - {dt})') - code_lines.append(f'{var_name} = {var_name} + _df{var_name}_dt * {dt} + ' - f'_dg{var_name}_dt * {dW_sb.name} + {high_order.name}') - else: - code_lines.append(f'{var_name} = {var_name} + _df{var_name}_dt * {dt} + ' - f'_dg{var_name}_dt * {dW_sb.name}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - return Euler.get_integral_step(diff_eq) - - -class MilsteinStra(Integrator): - """Heun two-stage stochastic numerical method for Stratonovich integral. - - Use the Stratonovich Heun algorithm to integrate Stratonovich equation, - according to paper [14]_ [15]_. - - .. math:: - Y_{n+1} &= Y_n + f_n h + {1 \\over 2}[g_n + g(\\overline{Y}_n)] \\Delta W_n - - \\overline{Y}_n &= Y_n + g_n \\Delta W_n - - - Or, it is written as - - .. math:: - - Y_1 &= y_n + f(y_n)h + g_n \\Delta W_n - - y_{n+1} &= y_n + {1 \over 2}[f(y_n) + f(Y_1)]h + {1 \\over 2} [g(y_n) + g(Y_1)] \\Delta W_n - - - Parameters - ---------- - diff_eq : DiffEquation - The differential equation. - - Returns - ------- - func : callable - The one-step numerical integrator function. - - References - ---------- - - .. [14] H. Gilsing and T. Shardlow, SDELab: A package for solving stochastic differential - equations in MATLAB, Journal of Computational and Applied Mathematics 205 (2007), - no. 2, 1002-1018. - .. [15] P.reversal_potential. Kloeden, reversal_potential. Platen, and H. Schurz, Numerical solution of SDE through computer - experiments, Springer, 1994. - - See Also - -------- - MilsteinIto - - """ - - def __init__(self, diff_eq): - super(MilsteinStra, self).__init__(diff_eq) - self._update_code = self.get_integral_step(diff_eq) - self._compile() - - @staticmethod - def get_integral_step(diff_eq, *args): - if diff_eq.is_stochastic: - if diff_eq.is_functional_noise: - g_dependent_on_var = diff_eq.replace_f_expressions('test', y_sub=f'test') - if len(g_dependent_on_var) == 0: - return Euler.get_integral_step(diff_eq) - - dt = profile.get_dt() - var_name = diff_eq.var_name - - # k1 part # - # ------- # - - # df - f_k1_expressions = diff_eq.get_f_expressions(substitute_vars=None) - code_lines = [str(expr) for expr in f_k1_expressions] # _df{var_name}_dt - - # dg - dW_sb = sympy.Symbol(f'_{var_name}_dW') - noise = f'_normal_like_({var_name})' - code_lines.append(f'{dW_sb.name} = sqrt({dt}) * {noise}') - g_k1_expressions = diff_eq.get_g_expressions() - code_lines.extend([str(expr) for expr in g_k1_expressions]) # _dg{var_name}_dt - - # high order part # - # --------------- # - - k1_expr = f'_k1 = {var_name} + _df{var_name}_dt * {dt} + ' \ - f'_dg{var_name}_dt * sqrt({dt})' - high_order = sympy.Symbol(f'_dg{var_name}_high_order') - g_k2_expressions = diff_eq.replace_g_expressions('k2', y_sub=f'_k1') - if len(g_k2_expressions): - code_lines.append(k1_expr) - code_lines.extend([str(expr) for expr in g_k2_expressions[:-1]]) - code_lines.append(f'_dg{var_name}_dt_k2 = {g_k2_expressions[-1].code}') - code_lines.append(f'{high_order.name} = 0.5 / sqrt({dt}) * ' - f'(_dg{var_name}_dt_k2 - _dg{var_name}_dt) *' - f'{dW_sb.name} * {dW_sb.name}') - code_lines.append(f'{var_name} = {var_name} + _df{var_name}_dt * {dt} + ' - f'_dg{var_name}_dt * {dW_sb.name} + {high_order.name}') - else: - code_lines.append(f'{var_name} = {var_name} + _df{var_name}_dt * {dt} + ' - f'_dg{var_name}_dt * {dW_sb.name}') - - # multiple returns - return_expr = ', '.join([var_name] + diff_eq.return_intermediates) - code_lines.append(f'_res = {return_expr}') - - # final - code = '\n'.join(code_lines) - subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} - code = tools.word_replace(code, subs_dict) - return code - - return Euler.get_integral_step(diff_eq) diff --git a/brainpy/integrators/__init__.py b/brainpy/integrators/__init__.py new file mode 100644 index 00000000..2305f132 --- /dev/null +++ b/brainpy/integrators/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from . import ode +from . import sde +from . import dde +from . import fde +from .delay_vars import * +from .integrate_wrapper import * +from .constants import * diff --git a/brainpy/integrators/ast_analysis.py b/brainpy/integrators/ast_analysis.py new file mode 100644 index 00000000..fb502893 --- /dev/null +++ b/brainpy/integrators/ast_analysis.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- + +import ast + +from brainpy import errors +from brainpy import tools + + +__all__ = [ + 'DiffEqReader', + 'separate_variables', +] + + +class DiffEqReader(ast.NodeVisitor): + """Read the code lines which defines the logic of a differential equation system. + + Currently, DiffEqReader cannot handle the for loop, and if-else condition. + Also, it do not assign values by a functional call. Like this: + + .. code-block:: python + + func(a, b, c) + + Instead, you should code like: + + .. code-block:: python + + c = func(a, b) + + Therefore, this class only has minimum power to analyze differential + equations. For example, this class may help to automatically find out + the linear part of a differential equation, thus forming the + Exponential Euler numerical methods. + """ + + def __init__(self): + self.code_lines = [] # list of str + self.variables = [] # list of list + self.returns = [] # list of str + self.rights = [] # list of str + + @staticmethod + def visit_container(nodes): + variables = [] + for var in nodes: + if isinstance(var, (ast.List, ast.Tuple)): + variables.extend(DiffEqReader.visit_container(var.elts)) + elif isinstance(var, ast.Name): + variables.extend(var.id) + else: + raise ValueError(f'Unknown target type: {var}') + return variables + + def visit_Assign(self, node): + variables = [] + for target in node.targets: + if isinstance(target, (ast.List, ast.Tuple)): + variables.extend(self.visit_container(target.elts)) + elif isinstance(target, ast.Name): + variables.append(target.id) + else: + raise ValueError(f'Unknown target type: {target}') + self.variables.append(variables) + self.code_lines.append(tools.ast2code(ast.fix_missing_locations(node))) + self.rights.append(tools.ast2code(ast.fix_missing_locations(node.value))) + return node + + def visit_AugAssign(self, node): + var = node.target.id + self.variables.append(var) + expr = tools.ast2code(ast.fix_missing_locations(node)) + self.code_lines.append(expr) + self.rights.append(tools.ast2code(ast.fix_missing_locations(node.value))) + return node + + def visit_Return(self, node): + if isinstance(node.value, ast.Name): + self.returns.append(node.value.id) + elif isinstance(node.value, (ast.Tuple, ast.List)): + for var in node.value.elts: + if not (var, ast.Name): + raise errors.DiffEqError(f'Unknown return type: {node}') + self.returns.append(var.id) + else: + raise errors.DiffEqError(f'Unknown return type: {node}') + return node + + def visit_AnnAssign(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support an ' + f'assignment with a type annotation.') + + def visit_If(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "if-else" conditions in differential equation.') + + def visit_IfExp(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "if-else" conditions in differential equation.') + + def visit_For(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "for" loops in differential equation.') + + def visit_While(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "while" loops in differential equation.') + + def visit_Try(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "try" handler in differential equation.') + + def visit_With(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "with" block in differential equation.') + + def visit_Raise(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "raise" statement in differential equation.') + + def visit_Delete(self, node): + raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' + f'analyze "del" operation in differential equation.') + + +def separate_variables(returns, variables, right_exprs, code_lines): + """Separate the expressions in a differential equation for each variable. + + For example, take the HH neuron model as an example: + + >>> eq_code = ''' + >>> def integral(V, m, h, n, t, Iext): + >>> alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + >>> beta = 4.0 * np.exp(-(V + 65) / 18) + >>> dmdt = alpha * (1 - m) - beta * m + >>> + >>> alpha = 0.07 * np.exp(-(V + 65) / 20.) + >>> beta = 1 / (1 + np.exp(-(V + 35) / 10)) + >>> dhdt = alpha * (1 - h) - beta * h + >>> return dmdt, dhdt + >>> ''' + >>> analyser = DiffEqReader() + >>> analyser.visit(ast.parse(eq_code)) + >>> separate_variables(returns=analyser.returns, + >>> variables=analyser.variables, + >>> right_exprs=analyser.rights, + >>> code_lines=analyser.code_lines) + {'dhdt': ['alpha = 0.07 * np.exp(-(V + 65) / 20.0)\n', + 'beta = 1 / (1 + np.exp(-(V + 35) / 10))\n', + 'dhdt = alpha * (1 - h) - beta * h\n'], + 'dmdt': ['alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10))\n', + 'beta = 4.0 * np.exp(-(V + 65) / 18)\n', + 'dmdt = alpha * (1 - m) - beta * m\n']} + + Parameters + ---------- + returns : list of str + The return expressions. + variables : list of list + The variables on each code line. + right_exprs : list of str + The right expression for each code line. + code_lines : list of str + The code lines in the differential equations. + + Returns + ------- + expressions_for_returns : dict + The expressions for each return variable. + """ + return_requires = {r: tools.get_identifiers(r) for r in returns} + expressions_for_returns = {r: [] for r in returns} + + length = len(variables) + reverse_ids = [i-length for i in range(length)] + reverse_ids = reverse_ids[::-1] + for r in expressions_for_returns.keys(): + for rid in reverse_ids: + dep = [] + for v in variables[rid]: + if v in return_requires[r]: + dep.append(v) + if len(dep): + expressions_for_returns[r].append(code_lines[rid]) + expr = right_exprs[rid] + return_requires[r].update(tools.get_identifiers(expr)) + for d in dep: + return_requires[r].remove(d) + for r, v in expressions_for_returns.items(): + expressions_for_returns[r] = v[::-1] + + return expressions_for_returns + diff --git a/brainpy/integrators/constants.py b/brainpy/integrators/constants.py new file mode 100644 index 00000000..b8c3ef2d --- /dev/null +++ b/brainpy/integrators/constants.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + + +__all__ = [ + 'SUPPORTED_VAR_TYPE', + 'SCALAR_VAR', + 'POPU_VAR', + 'SYSTEM_VAR', + + 'SUPPORTED_WIENER_TYPE', + 'SCALAR_WIENER', + 'VECTOR_WIENER', + + 'SUPPORTED_SDE_TYPE', + 'ITO_SDE', + 'STRA_SDE', + + 'NAME_PREFIX', +] + +# Ito SDE +# --- +# +ITO_SDE = 'Ito' + +# Stratonovich SDE +# --- +# +STRA_SDE = 'Stratonovich' + +SUPPORTED_SDE_TYPE = [ + ITO_SDE, + STRA_SDE +] + +# Scalar Wiener process +# ---- +# +SCALAR_WIENER = 'scalar_wiener' + +# Vector Wiener process +# ---- +# +VECTOR_WIENER = 'vector_wiener' + +SUPPORTED_WIENER_TYPE = [ + SCALAR_WIENER, + VECTOR_WIENER +] + +# Denotes each variable is a scalar variable +# ------- +# For example: +# +# def derivative(a, b, t): +# ... +# return da, db +# +# The "a" and "b" are scalars: a=1, b=2 +# +SCALAR_VAR = 'scalar' + +# Denotes each variable is a homogeneous population +# ------- +# For example: +# +# def derivative(a, b, t): +# ... +# return da, db +# +# The "a" and "b" are vectors or matrix: +# a = np.array([1,2]), b = np.array([3,4]) +# or, +# a = np.array([[1,2], [2,1]]), b=np.array([[3,4], [4,3]]) +# +POPU_VAR = 'population' + +# Denotes each variable is a system +# ------ +# For example, the above defined differential equations can be defined as: +# +# def derivative(x, t): +# a, b = x +# ... +# dx = np.array([da, db]) +# return dx +SYSTEM_VAR = 'system' + +SUPPORTED_VAR_TYPE = [ + SCALAR_VAR, + POPU_VAR, + SYSTEM_VAR, +] + +NAME_PREFIX = '_brainpy_numint_of_' diff --git a/brainpy/integrators/dde/__init__.py b/brainpy/integrators/dde/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/brainpy/integrators/dde/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/brainpy/integrators/delay_vars.py b/brainpy/integrators/delay_vars.py new file mode 100644 index 00000000..3d26b0ef --- /dev/null +++ b/brainpy/integrators/delay_vars.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + + +import abc +import math + +from brainpy import backend +from brainpy import profile + +__all__ = [ + 'AbstractDelay', + 'ConstantDelay', + 'VaryingDelay', + 'NeutralDelay', +] + + +class AbstractDelay(abc.ABC): + def __setitem__(self, time, value): + pass + + def __getitem__(self, time): + pass + + +class ConstantDelay(AbstractDelay): + def __init__(self, size, delay_len, before_t0): + # check size + if isinstance(size, int): + size = (size,) + if not isinstance(size, (tuple, list)): + raise ValueError('"size" must be an int, or a list/tuple of int.') + + # check delay_len + dt = profile.get_dt() + num_delay = int(math.ceil(delay_len / dt)) + + # delay data + self.data = backend.zeros((num_delay,) + size) + + # check defore_t0 + if callable(before_t0): + for i in range(num_delay): + self.data[i] = before_t0((i - num_delay) * dt) + else: + self.data[:] = before_t0 + + # other variables + self._delay_in = 0 + self._delay_out = ... + + def __setitem__(self, time, value): + pass + + def __getitem__(self, time): + pass + + +class VaryingDelay(AbstractDelay): + def __init__(self): + pass + + +class NeutralDelay(AbstractDelay): + def __init__(self): + pass diff --git a/brainpy/integrators/fde/__init__.py b/brainpy/integrators/fde/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/brainpy/integrators/fde/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/brainpy/integrators/integrate_wrapper.py b/brainpy/integrators/integrate_wrapper.py new file mode 100644 index 00000000..b5307233 --- /dev/null +++ b/brainpy/integrators/integrate_wrapper.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from . import ode +from . import sde + +__all__ = [ + 'odeint', + 'sdeint', + 'ddeint', + 'fdeint', +] + +supported_ode = [m for m in dir(ode) if not m.startswith('__')] +supported_sde = [m for m in dir(sde) if not m.startswith('__')] + + +def odeint(f=None, method=None, **kwargs): + def wrapper(f, ode_type, **kwargs): + integrator = getattr(ode, ode_type) + return integrator(f, **kwargs) + + if method is None: + method = 'euler' + if method not in supported_ode: + raise ValueError(f'Unknown ODE numerical method "{method}". Currently ' + f'BrainPy only support: {supported_ode}') + + if f is None: + return lambda f: wrapper(f, method, **kwargs) + else: + return wrapper(f, method, **kwargs) + + +def sdeint(f=None, method=None, **kwargs): + def wrapper(f, ode_type, **kwargs): + integrator = getattr(sde, ode_type) + return integrator(f, **kwargs) + + if method is None: + method = 'euler' + if method not in supported_sde: + raise ValueError(f'Unknown SDE numerical method "{method}". Currently ' + f'BrainPy only support: {supported_sde}') + + if f is None: + return lambda f: wrapper(f, method, **kwargs) + else: + return wrapper(f, method, **kwargs) + + +def ddeint(): + pass + + +def fdeint(): + pass diff --git a/brainpy/integrators/ode/__init__.py b/brainpy/integrators/ode/__init__.py new file mode 100644 index 00000000..0f2b2354 --- /dev/null +++ b/brainpy/integrators/ode/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +""" +Numerical methods for ordinary differential equations. +""" + +from .rk_adaptive_methods import * +from .rk_methods import * +# from .other_methods import * + diff --git a/brainpy/integrators/ode/exp_euler.py b/brainpy/integrators/ode/exp_euler.py new file mode 100644 index 00000000..1ac741d9 --- /dev/null +++ b/brainpy/integrators/ode/exp_euler.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +from brainpy import profile +from brainpy import backend + +__all__ = [ + 'exponential_euler', +] + + +def exponential_euler(f, return_linear_term=False): + dt = profile.get_dt() + + def int_f(x, t, *args): + df, linear_part = f(x, t, *args) + y = x + (backend.exp(linear_part * dt) - 1) / linear_part * df + return y + + return int_f diff --git a/brainpy/integrators/ode/rk_adaptive_methods.py b/brainpy/integrators/ode/rk_adaptive_methods.py new file mode 100644 index 00000000..7e3c1a8b --- /dev/null +++ b/brainpy/integrators/ode/rk_adaptive_methods.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- + +""" +https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods +https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods +""" + +from brainpy import profile +from .wrapper import adaptive_rk_wrapper +from brainpy.integrators import utils + +__all__ = [ + 'rkf45', + 'rkf12', + 'rkdp', + 'ck', + 'bs', + 'heun_euler' +] + + +def _base(A, B1, B2, C, f=None, tol=None, adaptive=None, + dt=None, show_code=None, var_type=None): + """ + + Parameters + ---------- + A : + B1 : + B2 : + C : + f : + tol : + adaptive : + dt : + show_code : + var_type : + + Returns + ------- + + """ + adaptive = False if (adaptive is None) else adaptive + dt = profile.get_dt() if (dt is None) else dt + tol = 0.1 if tol is None else tol + show_code = False if tol is None else show_code + var_type = utils.POPU_VAR if var_type is None else var_type + + if f is None: + return lambda f: adaptive_rk_wrapper(f, dt=dt, A=A, B1=B1, B2=B2, C=C, tol=tol, + adaptive=adaptive, show_code=show_code, + var_type=var_type) + else: + return adaptive_rk_wrapper(f, dt=dt, A=A, B1=B1, B2=B2, C=C, tol=tol, + adaptive=adaptive, show_code=show_code, + var_type=var_type) + + +def rkf45(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=None): + """The Runge–Kutta–Fehlberg method for ordinary differential equations. + + The method presented in Fehlberg's 1969 paper has been dubbed the + RKF45 method, and is a method of order :math:`O(h^4)` with an error + estimator of order :math:`O(h^5)`. The novelty of Fehlberg's method is + that it is an embedded method from the Runge–Kutta family, meaning that + identical function evaluations are used in conjunction with each other + to create methods of varying order and similar error constants. + + It has the characteristics of: + + - method stage = 6 + - method order = 5 + - Butcher Tables: + + .. math:: + + \\begin{array}{l|lllll} + 0 & & & & & & \\\\ + 1 / 4 & 1 / 4 & & & & \\\\ + 3 / 8 & 3 / 32 & 9 / 32 & & \\\\ + 12 / 13 & 1932 / 2197 & -7200 / 2197 & 7296 / 2197 & \\\\ + 1 & 439 / 216 & -8 & 3680 / 513 & -845 / 4104 & & \\\\ + 1 / 2 & -8 / 27 & 2 & -3544 / 2565 & 1859 / 4104 & -11 / 40 & \\\\ + \\hline & 16 / 135 & 0 & 6656 / 12825 & 28561 / 56430 & -9 / 50 & 2 / 55 \\\\ + & 25 / 216 & 0 & 1408 / 2565 & 2197 / 4104 & -1 / 5 & 0 + \\end{array} + + References + ---------- + + [1] https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta%E2%80%93Fehlberg_method + [2] Erwin Fehlberg (1969). Low-order classical Runge-Kutta formulas with step + size control and their application to some heat transfer problems . NASA + Technical Report 315. + https://ntrs.nasa.gov/api/citations/19690021375/downloads/19690021375.pdf + + """ + + A = [(), (0.25,), (0.09375, 0.28125), + ('1932/2197', '-7200/2197', '7296/2197'), + ('439/216', -8, '3680/513', '-845/4104'), + ('-8/27', 2, '-3544/2565', '1859/4104', -0.275)] + B1 = ['16/135', 0, '6656/12825', '28561/56430', -0.18, '2/55'] + B2 = ['25/216', 0, '1408/2565', '2197/4104', -0.2, 0] + C = [0, 0.25, 0.375, '12/13', 1, '1/3'] + + return _base(A=A, B1=B1, B2=B2, C=C, f=f, dt=dt, tol=tol, + adaptive=adaptive, show_code=show_code, var_type=var_type) + + + +def rkf12(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=None): + """The Fehlberg RK1(2) method for ordinary differential equations. + + The Fehlberg method has two methods of orders 1 and 2. + + It has the characteristics of: + + - method stage = 2 + - method order = 1 + - Butcher Tables: + + .. math:: + + \\begin{array}{l|ll} + 0 & & \\\\ + 1 / 2 & 1 / 2 & \\\\ + 1 & 1 / 256 & 255 / 256 & \\\\ + \\hline & 1 / 512 & 255 / 256 & 1 / 512 \\\\ + & 1 / 256 & 255 / 256 & 0 + \\end{array} + + References + ---------- + + .. [1] Fehlberg, E. (1969-07-01). "Low-order classical Runge-Kutta + formulas with stepsize control and their application to some heat + transfer problems" + + """ + + A = [(), (0.5,), ('1/256', '255/256')] + B1 = ['1/512', '255/256', '1/512'] + B2 = ['1/256', '255/256', 0] + C = [0, 0.5, 1] + + return _base(A=A, B1=B1, B2=B2, C=C, f=f, dt=dt, tol=tol, + adaptive=adaptive, show_code=show_code, var_type=var_type) + + +def rkdp(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=None): + """The Dormand–Prince method for ordinary differential equations. + + The DOPRI method, is an explicit method for solving ordinary differential equations + (Dormand & Prince 1980). The Dormand–Prince method has seven stages, but it uses only + six function evaluations per step because it has the FSAL (First Same As Last) property: + the last stage is evaluated at the same point as the first stage of the next step. + Dormand and Prince chose the coefficients of their method to minimize the error of + the fifth-order solution. This is the main difference with the Fehlberg method, which + was constructed so that the fourth-order solution has a small error. For this reason, + the Dormand–Prince method is more suitable when the higher-order solution is used to + continue the integration, a practice known as local extrapolation + (Shampine 1986; Hairer, Nørsett & Wanner 2008, pp. 178–179). + + It has the characteristics of: + + - method stage = 7 + - method order = 5 + - Butcher Tables: + + .. math:: + + \\begin{array}{l|llllll} + 0 & \\\\ + 1 / 5 & 1 / 5 & & & \\\\ + 3 / 10 & 3 / 40 & 9 / 40 & & & \\\\ + 4 / 5 & 44 / 45 & -56 / 15 & 32 / 9 & & \\\\ + 8 / 9 & 19372 / 6561 & -25360 / 2187 & 64448 / 6561 & -212 / 729 & \\\\ + 1 & 9017 / 3168 & -355 / 33 & 46732 / 5247 & 49 / 176 & -5103 / 18656 & \\\\ + 1 & 35 / 384 & 0 & 500 / 1113 & 125 / 192 & -2187 / 6784 & 11 / 84 & \\\\ + \\hline & 35 / 384 & 0 & 500 / 1113 & 125 / 192 & -2187 / 6784 & 11 / 84 & 0 \\\\ + & 5179 / 57600 & 0 & 7571 / 16695 & 393 / 640 & -92097 / 339200 & 187 / 2100 & 1 / 40 + \\end{array} + + References + ---------- + + [1] https://en.wikipedia.org/wiki/Dormand%E2%80%93Prince_method + [2] Dormand, J. R.; Prince, P. J. (1980), "A family of embedded Runge-Kutta formulae", + Journal of Computational and Applied Mathematics, 6 (1): 19–26, + doi:10.1016/0771-050X(80)90013-3. + """ + + A = [(), (0.2,), (0.075, 0.225), + ('44/45', '-56/15', '32/9'), + ('19372/6561', '-25360/2187', '64448/6561', '-212/729'), + ('9017/3168', '-355/33', '46732/5247', '49/176', '-5103/18656'), + ('35/384', 0, '500/1113', '125/192', '-2187/6784', '11/84')] + B1 = ['35/384', 0, '500/1113', '125/192', '-2187/6784', '11/84', 0] + B2 = ['5179/57600', 0, '7571/16695', '393/640', '-92097/339200', '187/2100', 0.025] + C = [0, 0.2, 0.3, 0.8, '8/9', 1, 1] + + return _base(A=A, B1=B1, B2=B2, C=C, f=f, dt=dt, tol=tol, + adaptive=adaptive, show_code=show_code, var_type=var_type) + + +def ck(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=None): + """The Cash–Karp method for ordinary differential equations. + + The Cash–Karp method was proposed by Professor Jeff R. Cash from Imperial College London + and Alan H. Karp from IBM Scientific Center. it uses six function evaluations to calculate + fourth- and fifth-order accurate solutions. The difference between these solutions is then + taken to be the error of the (fourth order) solution. This error estimate is very convenient + for adaptive stepsize integration algorithms. + + It has the characteristics of: + + - method stage = 6 + - method order = 4 + - Butcher Tables: + + .. math:: + + \\begin{array}{l|lllll} + 0 & & & & & & \\\\ + 1 / 5 & 1 / 5 & & & & & \\\\ + 3 / 10 & 3 / 40 & 9 / 40 & & & \\\\ + 3 / 5 & 3 / 10 & -9 / 10 & 6 / 5 & & \\\\ + 1 & -11 / 54 & 5 / 2 & -70 / 27 & 35 / 27 & & \\\\ + 7 / 8 & 1631 / 55296 & 175 / 512 & 575 / 13824 & 44275 / 110592 & 253 / 4096 & \\\\ + \\hline & 37 / 378 & 0 & 250 / 621 & 125 / 594 & 0 & 512 / 1771 \\\\ + & 2825 / 27648 & 0 & 18575 / 48384 & 13525 / 55296 & 277 / 14336 & 1 / 4 + \\end{array} + + References + ---------- + + [1] https://en.wikipedia.org/wiki/Cash%E2%80%93Karp_method + [2] J. R. Cash, A. H. Karp. "A variable order Runge-Kutta method for initial value + problems with rapidly varying right-hand sides", ACM Transactions on Mathematical + Software 16: 201-222, 1990. doi:10.1145/79505.79507 + """ + + A = [(), (0.2,), (0.075, 0.225), (0.3, -0.9, 1.2), + ('-11/54', 2.5, '-70/27', '35/27'), + ('1631/55296', '175/512', '575/13824', '44275/110592', '253/4096')] + B1 = ['37/378', 0, '250/621', '125/594', 0, '512/1771'] + B2 = ['2825/27648', 0, '18575/48384', '13525/55296', '277/14336', 0.25] + C = [0, 0.2, 0.3, 0.6, 1, 0.875] + + return _base(A=A, B1=B1, B2=B2, C=C, f=f, dt=dt, tol=tol, + adaptive=adaptive, show_code=show_code, var_type=var_type) + + +def bs(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=None): + """The Bogacki–Shampine method for ordinary differential equations. + + The Bogacki–Shampine method was proposed by Przemysław Bogacki and Lawrence F. + Shampine in 1989 (Bogacki & Shampine 1989). The Bogacki–Shampine method is a + Runge–Kutta method of order three with four stages with the First Same As Last + (FSAL) property, so that it uses approximately three function evaluations per + step. It has an embedded second-order method which can be used to implement adaptive step size. + + It has the characteristics of: + + - method stage = 4 + - method order = 3 + - Butcher Tables: + + .. math:: + + \\begin{array}{l|lll} + 0 & & & \\\\ + 1 / 2 & 1 / 2 & & \\\\ + 3 / 4 & 0 & 3 / 4 & \\\\ + 1 & 2 / 9 & 1 / 3 & 4 / 9 \\\\ + \\hline & 2 / 9 & 1 / 3 & 4 / 90 \\\\ + & 7 / 24 & 1 / 4 & 1 / 3 & 1 / 8 + \\end{array} + + References + ---------- + + [1] https://en.wikipedia.org/wiki/Bogacki%E2%80%93Shampine_method + [2] Bogacki, Przemysław; Shampine, Lawrence F. (1989), "A 3(2) pair of Runge–Kutta + formulas", Applied Mathematics Letters, 2 (4): 321–325, doi:10.1016/0893-9659(89)90079-7 + """ + + A = [(), (0.5,), (0., 0.75), ('2/9', '1/3', '4/0'), ] + B1 = ['2/9', '1/3', '4/9', 0] + B2 = ['7/24', 0.25, '1/3', 0.125] + C = [0, 0.5, 0.75, 1] + + return _base(A=A, B1=B1, B2=B2, C=C, f=f, dt=dt, tol=tol, + adaptive=adaptive, show_code=show_code, var_type=var_type) + + +def heun_euler(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=None): + """The Heun–Euler method for ordinary differential equations. + + The simplest adaptive Runge–Kutta method involves combining Heun's method, + which is order 2, with the Euler method, which is order 1. + + It has the characteristics of: + + - method stage = 2 + - method order = 1 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cc} + 0&\\\\ + 1& 1 \\\\ + \\hline + & 1/2& 1/2\\\\ + & 1 & 0 + \\end{array} + + """ + + A = [(), (1,)] + B1 = [0.5, 0.5] + B2 = [1, 0] + C = [0, 1] + + return _base(A=A, B1=B1, B2=B2, C=C, f=f, dt=dt, tol=tol, + adaptive=adaptive, show_code=show_code, var_type=var_type) + + +def DOP853(f=None, tol=None, adaptive=None, dt=None, show_code=None, each_var_is_scalar=None): + """The DOP853 method for ordinary differential equations. + + DOP853 is an explicit Runge-Kutta method of order 8(5,3) due to Dormand & Prince + (with stepsize control and dense output). + + + References + ---------- + + [1] E. Hairer, S.P. Norsett and G. Wanner, "Solving ordinary Differential Equations + I. Nonstiff Problems", 2nd edition. Springer Series in Computational Mathematics, + Springer-Verlag (1993). + [2] http://www.unige.ch/~hairer/software.html + """ + pass diff --git a/brainpy/integrators/ode/rk_methods.py b/brainpy/integrators/ode/rk_methods.py new file mode 100644 index 00000000..152a124f --- /dev/null +++ b/brainpy/integrators/ode/rk_methods.py @@ -0,0 +1,380 @@ +# -*- coding: utf-8 -*- + +""" +https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods#Kutta's_third-order_method +""" + +from brainpy import profile +from brainpy.integrators import utils +from .wrapper import rk_wrapper + +__all__ = [ + 'euler', + 'midpoint', + 'heun2', + 'ralston2', + 'rk2', + 'rk3', + 'heun3', + 'ralston3', + 'ssprk3', + 'rk4', + 'ralston4', + 'rk4_38rule', +] + + +def _base(A, B, C, f, show_code, dt): + dt = profile.get_dt() if dt is None else dt + show_code = False if show_code is None else show_code + + if f is None: + return lambda f: rk_wrapper(f, show_code=show_code, dt=dt, A=A, B=B, C=C) + else: + return rk_wrapper(f, show_code=show_code, dt=dt, A=A, B=B, C=C) + + +def euler(f=None, show_code=None, dt=None): + """The Euler method is first order. The lack of stability + and accuracy limits its popularity mainly to use as a + simple introductory example of a numeric solution method. + """ + A = [(), ] + B = [1] + C = [0] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def midpoint(f=None, show_code=None, dt=None): + """midpoint method for ordinary differential equations. + + The (explicit) midpoint method is a second-order method + with two stages. + + It has the characteristics of: + + - method stage = 2 + - method order = 2 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cc} + 0 & 0 & 0 \\\\ + 1 / 2 & 1 / 2 & 0 \\\\ + \\hline & 0 & 1 + \\end{array} + + """ + A = [(), (0.5,)] + B = [0, 1] + C = [0, 0.5] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def heun2(f=None, show_code=None, dt=None): + """Heun's method for ordinary differential equations. + + Heun's method is a second-order method with two stages. + It is also known as the explicit trapezoid rule, improved + Euler's method, or modified Euler's method. + + It has the characteristics of: + + - method stage = 2 + - method order = 2 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cc} + 0.0 & 0.0 & 0.0 \\\\ + 1.0 & 1.0 & 0.0 \\\\ + \\hline & 0.5 & 0.5 + \\end{array} + + """ + A = [(), (1,)] + B = [0.5, 0.5] + C = [0, 1] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def ralston2(f=None, show_code=None, dt=None): + """Ralston's method for ordinary differential equations. + + Ralston's method is a second-order method with two stages and + a minimum local error bound. + + It has the characteristics of: + + - method stage = 2 + - method order = 2 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cc} + 0 & 0 & 0 \\\\ + 2 / 3 & 2 / 3 & 0 \\\\ + \\hline & 1 / 4 & 3 / 4 + \\end{array} + """ + A = [(), ('2/3',)] + B = [0.25, 0.75] + C = [0, '2/3'] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def _rk2_wrapper(f, show_code, dt, beta): + vars, other_args, org_args = utils.get_args(f) + + code_scope = {f.__name__: f, 'dt': dt, 'beta': beta, + 'k1': 1 - 1 / (2 * beta), 'k2': 1 / (2 * beta)} + code_lines = [f'def int_{f.__name__}({", ".join(org_args)}):'] + # k1 + k1_args = vars + other_args + k1_vars_d = [f'd{v}_k1' for v in vars] + code_lines.append(f' {", ".join(k1_vars_d)} = {f.__name__}({", ".join(k1_args)})') + # k2 + k2_args = [f'{v} + d{v}_k1 * dt * beta' for v in vars] + k2_args.append('t + dt * beta') + k2_args.extend(other_args[1:]) + k2_vars_d = [f'd{v}_k2' for v in vars] + code_lines.append(f' {", ".join(k2_vars_d)} = {f.__name__}({", ".join(k2_args)})') + # returns + for v, k1, k2 in zip(vars, k1_vars_d, k2_vars_d): + code_lines.append(f' {v}_new = {v} + ({k1} * k1 + {k2} * k2) * dt') + return_vars = [f'{v}_new' for v in vars] + code_lines.append(f' return {", ".join(return_vars)}') + + code = '\n'.join(code_lines) + if show_code: + print(code) + print(code_scope) + exec(compile(code, '', 'exec'), code_scope) + return code_scope[f'int_{f.__name__}'] + + +def rk2(f=None, show_code=None, dt=None, beta=None): + """Runge–Kutta methods for ordinary differential equations. + + Generic second-order method. + + It has the characteristics of: + + - method stage = 2 + - method order = 2 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cc} + 0 & 0 & 0 \\\\ + \\beta & \\beta & 0 \\\\ + \\hline & 1 - {1 \\over 2 * \\beta} & {1 \over 2 * \\beta} + \\end{array} + """ + beta = 2 / 3 if beta is None else beta + dt = profile.get_dt() if dt is None else dt + show_code = False if show_code is None else show_code + + if f is None: + return lambda f: _rk2_wrapper(f, show_code=show_code, dt=dt, beta=beta) + else: + return _rk2_wrapper(f, show_code=show_code, dt=dt, beta=beta) + + +def rk3(f=None, show_code=None, dt=None): + """Classical third-order Runge-Kutta method for ordinary differential equations. + + It has the characteristics of: + + - method stage = 3 + - method order = 3 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|ccc} + 0 & 0 & 0 & 0 \\\\ + 1 / 2 & 1 / 2 & 0 & 0 \\\\ + 1 & -1 & 2 & 0 \\\\ + \\hline & 1 / 6 & 2 / 3 & 1 / 6 + \\end{array} + + """ + A = [(), (0.5,), (-1, 2)] + B = ['1/6', '2/3', '1/6'] + C = [0, 0.5, 1] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def heun3(f=None, show_code=None, dt=None): + """Heun's third-order method for ordinary differential equations. + + It has the characteristics of: + + - method stage = 3 + - method order = 3 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|ccc} + 0 & 0 & 0 & 0 \\\\ + 1 / 3 & 1 / 3 & 0 & 0 \\\\ + 2 / 3 & 0 & 2 / 3 & 0 \\\\ + \\hline & 1 / 4 & 0 & 3 / 4 + \\end{array} + + """ + A = [(), ('1/3',), (0, '2/3')] + B = [0.25, 0, 0.75] + C = [0, '1/3', '2/3'] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def ralston3(f=None, show_code=None, dt=None): + """Ralston's third-order method for ordinary differential equations. + + It has the characteristics of: + + - method stage = 3 + - method order = 3 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|ccc} + 0 & 0 & 0 & 0 \\\\ + 1 / 2 & 1 / 2 & 0 & 0 \\\\ + 3 / 4 & 0 & 3 / 4 & 0 \\\\ + \\hline & 2 / 9 & 1 / 3 & 4 / 9 + \\end{array} + + References + ---------- + + .. [1] Ralston, Anthony (1962). "Runge-Kutta Methods with Minimum Error Bounds". + Math. Comput. 16 (80): 431–437. doi:10.1090/S0025-5718-1962-0150954-0 + + """ + A = [(), (0.5,), (0, 0.75)] + B = ['2/9', '1/3', '4/9'] + C = [0, 0.5, 0.75] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def ssprk3(f=None, show_code=None, dt=None): + """Third-order Strong Stability Preserving Runge-Kutta (SSPRK3). + + It has the characteristics of: + + - method stage = 3 + - method order = 3 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|ccc} + 0 & 0 & 0 & 0 \\\\ + 1 & 1 & 0 & 0 \\\\ + 1 / 2 & 1 / 4 & 1 / 4 & 0 \\\\ + \\hline & 1 / 6 & 1 / 6 & 2 / 3 + \\end{array} + + """ + A = [(), (1,), (0.25, 0.25)] + B = ['1/6', '1/6', '2/3'] + C = [0, 1, 0.5] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def rk4(f=None, show_code=None, dt=None): + """Classical fourth-order Runge-Kutta method for ordinary differential equations. + + It has the characteristics of: + + - method stage = 4 + - method order = 4 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cccc} + 0 & 0 & 0 & 0 & 0 \\\\ + 1 / 2 & 1 / 2 & 0 & 0 & 0 \\\\ + 1 / 2 & 0 & 1 / 2 & 0 & 0 \\\\ + 1 & 0 & 0 & 1 & 0 \\\\ + \\hline & 1 / 6 & 1 / 3 & 1 / 3 & 1 / 6 + \\end{array} + + """ + + A = [(), (0.5,), (0., 0.5), (0., 0., 1)] + B = ['1/6', '1/3', '1/3', '1/6'] + C = [0, 0.5, 0.5, 1] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def ralston4(f=None, show_code=None, dt=None): + """Ralston's fourth-order method for ordinary differential equations. + + It has the characteristics of: + + - method stage = 4 + - method order = 4 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cccc} + 0 & 0 & 0 & 0 & 0 \\\\ + .4 & .4 & 0 & 0 & 0 \\\\ + .45573725 & .29697761 & .15875964 & 0 & 0 \\\\ + 1 & .21810040 & -3.05096516 & 3.83286476 & 0 \\\\ + \\hline & .17476028 & -.55148066 & 1.20553560 & .17118478 + \\end{array} + + References + ---------- + + [1] Ralston, Anthony (1962). "Runge-Kutta Methods with Minimum Error Bounds". + Math. Comput. 16 (80): 431–437. doi:10.1090/S0025-5718-1962-0150954-0 + + """ + A = [(), (.4,), (.29697761, .15875964), (.21810040, -3.05096516, 3.83286476)] + B = [.17476028, -.55148066, 1.20553560, .17118478] + C = [0, .4, .45573725, 1] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) + + +def rk4_38rule(f=None, show_code=None, dt=None): + """3/8-rule fourth-order method for ordinary differential equations. + + This method doesn't have as much notoriety as the "classical" method, + but is just as classical because it was proposed in the same paper + (Kutta, 1901). + + It has the characteristics of: + + - method stage = 4 + - method order = 4 + - Butcher Tables: + + .. math:: + + \\begin{array}{c|cccc} + 0 & 0 & 0 & 0 & 0 \\\\ + 1 / 3 & 1 / 3 & 0 & 0 & 0 \\\\ + 2 / 3 & -1 / 3 & 1 & 0 & 0 \\\\ + 1 & 1 & -1 & 1 & 0 \\\\ + \\hline & 1 / 8 & 3 / 8 & 3 / 8 & 1 / 8 + \\end{array} + + """ + A = [(), ('1/3',), ('-1/3', '1'), (1, -1, 1)] + B = ['1/8', '3/8', '3/8', '1/8'] + C = [0, '1/3', '2/3', 1] + return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) diff --git a/brainpy/integrators/ode/wrapper.py b/brainpy/integrators/ode/wrapper.py new file mode 100644 index 00000000..b0cde1ab --- /dev/null +++ b/brainpy/integrators/ode/wrapper.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +from brainpy.integrators import utils + +__all__ = [ + 'rk_wrapper', + 'adaptive_rk_wrapper', +] + +_ODE_UNKNOWN_NO = 0 + + +def _step(vars, dt_var, f_name, A, C, code_lines, other_args): + # steps + for si, sval in enumerate(A): + # k-step arguments + k_args = [] + for v in vars: + k_arg = f'{v}' + for j, sv in enumerate(sval): + if sv not in [0., '0.', '0']: + if sv in ['1.', '1', 1.]: + k_arg += f' + {dt_var} * d{v}_k{j + 1}' + else: + k_arg += f' + {dt_var} * d{v}_k{j + 1} * {sv}' + if k_arg != v: + name = f'k{si + 1}_{v}_arg' + code_lines.append(f' {name} = {k_arg}') + k_args.append(name) + else: + k_args.append(v) + + t_arg = 't' + if C[si] not in [0., '0.', '0']: + if C[si] in ['1.', '1', 1.]: + t_arg += f' + {dt_var}' + else: + t_arg += f' + {dt_var} * {C[si]}' + name = f'k{si + 1}_t_arg' + code_lines.append(f' {name} = {t_arg}') + k_args.append(name) + else: + k_args.append(f'{dt_var}') + + # k-step derivative names + k_derivatives = [f'd{v}_k{si + 1}' for v in vars] + + # k-step code line + code_lines.append(f' {", ".join(k_derivatives)} = {f_name}(' + f'{", ".join(k_args + other_args[1:])})') + + +def _update(vars, dt_var, B, code_lines): + return_args = [] + for v in vars: + result = v + for i, b1 in enumerate(B): + if b1 not in [0., '0.', '0']: + result += f' + d{v}_k{i + 1} * {dt_var} * {b1}' + code_lines.append(f' {v}_new = {result}') + return_args.append(f'{v}_new') + return return_args + + +def _compile(code_lines, code_scope, show_code): + code = '\n'.join(code_lines) + if show_code: + print(code) + print(code_scope) + print() + exec(compile(code, '', 'exec'), code_scope) + return code_scope + + +def rk_wrapper(f, show_code, dt, A, B, C): + """Runge–Kutta methods for ordinary differential equation. + + For the system, + + .. math:: + + \frac{d y}{d t}=f(t, y) + + + Explicit Runge-Kutta methods take the form + + .. math:: + + k_{i}=f\\left(t_{n}+c_{i}h,y_{n}+h\\sum _{j=1}^{s}a_{ij}k_{j}\\right) \\\\ + y_{n+1}=y_{n}+h \\sum_{i=1}^{s} b_{i} k_{i} + + Each method listed on this page is defined by its Butcher tableau, + which puts the coefficients of the method in a table as follows: + + .. math:: + + \\begin{array}{c|cccc} + c_{1} & a_{11} & a_{12} & \\ldots & a_{1 s} \\\\ + c_{2} & a_{21} & a_{22} & \\ldots & a_{2 s} \\\\ + \\vdots & \vdots & \vdots & \\ddots & \vdots \\\\ + c_{s} & a_{s 1} & a_{s 2} & \\ldots & a_{s s} \\\\ + \\hline & b_{1} & b_{2} & \\ldots & b_{s} + \\end{array} + + Parameters + ---------- + f : callable + The derivative function. + show_code : bool + Whether show the formatted code. + dt : float + The numerical precision. + A : tuple, list + The A matrix in the Butcher tableau. + B : tuple, list + The B vector in the Butcher tableau. + C : tuple, list + The C vector in the Butcher tableau. + + Returns + ------- + integral_func : callable + The one-step numerical integration function. + """ + class_kw, variables, parameters, arguments = utils.get_args(f) + dt_var = 'dt' + if f.__name__.isdentifier(): + f_name = f.__name__ + else: + global _ODE_UNKNOWN_NO + f_name = f'ode_unknown_{_ODE_UNKNOWN_NO}' + _ODE_UNKNOWN_NO += 1 + f_new_name = utils.NAME_PREFIX + f_name + + # code scope + code_scope = {f_name: f, 'dt': dt} + + # code lines + code_lines = [f'def {f_new_name}({", ".join(arguments)}):'] + # step stage + _step(variables, dt_var, f_name, A, C, code_lines, parameters) + # variable update + return_args = _update(variables, dt_var, B, code_lines) + + # returns + code_lines.append(f' return {", ".join(return_args)}') + + # compilation + _compile(code_lines, code_scope, show_code) + return code_scope[f_new_name] + + +def adaptive_rk_wrapper(f, dt, A, B1, B2, C, tol, adaptive, show_code, var_type): + """Adaptive Runge-Kutta numerical method for ordinary differential equations. + + The embedded methods are designed to produce an estimate of the local + truncation error of a single Runge-Kutta step, and as result, allow to + control the error with adaptive stepsize. This is done by having two + methods in the tableau, one with order p and one with order :math:`p-1`. + + The lower-order step is given by + + .. math:: + + y^*_{n+1} = y_n + h\\sum_{i=1}^s b^*_i k_i, + + where the :math:`k_{i}` are the same as for the higher order method. Then the error is + + .. math:: + + e_{n+1} = y_{n+1} - y^*_{n+1} = h\\sum_{i=1}^s (b_i - b^*_i) k_i, + + + which is :math:`O(h^{p})`. The Butcher Tableau for this kind of method is extended to + give the values of :math:`b_{i}^{*}` + + .. math:: + + \\begin{array}{c|cccc} + c_1 & a_{11} & a_{12}& \\dots & a_{1s}\\\\ + c_2 & a_{21} & a_{22}& \\dots & a_{2s}\\\\ + \\vdots & \\vdots & \\vdots& \\ddots& \\vdots\\\\ + c_s & a_{s1} & a_{s2}& \\dots & a_{ss} \\\\ + \\hline & b_1 & b_2 & \\dots & b_s\\\\ + & b_1^* & b_2^* & \\dots & b_s^*\\\\ + \\end{array} + + + Parameters + ---------- + f : callable + The derivative function. + show_code : bool + Whether show the formatted code. + dt : float + The numerical precision. + A : tuple, list + The A matrix in the Butcher tableau. + B1 : tuple, list + The B1 vector in the Butcher tableau. + B2 : tuple, list + The B2 vector in the Butcher tableau. + C : tuple, list + The C vector in the Butcher tableau. + adaptive : bool + tol : float + var_type : str + + Returns + ------- + integral_func : callable + The one-step numerical integration function. + """ + assert var_type in utils.SUPPORTED_VAR_TYPE, \ + f'"var_type" only supports {utils.SUPPORTED_VAR_TYPE}, not {var_type}.' + + class_kw, variables, parameters, arguments = utils.get_args(f) + dt_var = 'dt' + if f.__name__.isdentifier(): + f_name = f.__name__ + else: + global _ODE_UNKNOWN_NO + f_name = f'ode_unknown_{_ODE_UNKNOWN_NO}' + _ODE_UNKNOWN_NO += 1 + f_new_name = utils.NAME_PREFIX + f_name + + if adaptive: + # code scope + code_scope = {f_name: f, 'tol': tol} + arguments = list(arguments) + ['dt'] + else: + # code scope + code_scope = {f_name: f, 'dt': dt} + + # code lines + code_lines = [f'def {f_new_name}({", ".join(arguments)}):'] + # stage steps + _step(variables, dt_var, f_name, A, C, code_lines, parameters) + # variable update + return_args = _update(variables, dt_var, B1, code_lines) + + # error adaptive item + if adaptive: + errors = [] + for v in variables: + result = [] + for i, (b1, b2) in enumerate(zip(B1, B2)): + if isinstance(b1, str): + b1 = eval(b1) + if isinstance(b2, str): + b2 = eval(b2) + diff = b1 - b2 + if diff != 0.: + result.append(f'd{v}_k{i + 1} * {dt_var} * {diff}') + if len(result) > 0: + if var_type == utils.SCALAR_VAR: + code_lines.append(f' {v}_te = abs({" + ".join(result)})') + else: + code_lines.append(f' {v}_te = sum(abs({" + ".join(result)}))') + errors.append(f'{v}_te') + if len(errors) > 0: + code_lines.append(f' error = {" + ".join(errors)}') + code_lines.append(f' if error > tol:') + code_lines.append(f' {dt_var}_new = 0.9 * {dt_var} * (tol / error) ** 0.2') + code_lines.append(f' else:') + code_lines.append(f' {dt_var}_new = {dt_var}') + return_args.append(f'{dt_var}_new') + + # returns + code_lines.append(f' return {", ".join(return_args)}') + + # compilation + _compile(code_lines, code_scope, show_code) + return code_scope[f_new_name] diff --git a/brainpy/integrators/sde/__init__.py b/brainpy/integrators/sde/__init__.py new file mode 100644 index 00000000..eb0dbc9f --- /dev/null +++ b/brainpy/integrators/sde/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +""" +Numerical methods for stochastic differential equations. +""" + +from .euler_and_milstein import * +from .srk_scalar import * +from .srk_strong import * +# from .srk_weak import * + diff --git a/brainpy/integrators/sde/common.py b/brainpy/integrators/sde/common.py new file mode 100644 index 00000000..fc68ef32 --- /dev/null +++ b/brainpy/integrators/sde/common.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from pprint import pprint + +from brainpy.integrators import constants +from brainpy.integrators import utils + +_SDE_UNKNOWN_NO = 0 + + +def basic_info(f, g): + vdt = 'dt' + if f.__name__.isidentifier(): + func_name = f.__name__ + elif g.__name__.isidentifier(): + func_name = g.__name__ + else: + global _SDE_UNKNOWN_NO + func_name = f'unknown_sde{_SDE_UNKNOWN_NO}' + func_new_name = constants.NAME_PREFIX + func_name + class_kw, variables, parameters, arguments = utils.get_args(f) + + return vdt, variables, parameters, arguments, func_new_name + + +def return_and_compile(code_lines, code_scope, show_code, variables): + # returns + new_vars = [f'{var}_new' for var in variables] + code_lines.append(f' return {", ".join(new_vars)}') + + # compile + code = '\n'.join(code_lines) + if show_code: + print(code) + print() + pprint(code_scope) + print() + exec(compile(code, '', 'exec'), code_scope) + diff --git a/brainpy/integrators/sde/euler_and_milstein.py b/brainpy/integrators/sde/euler_and_milstein.py new file mode 100644 index 00000000..14dfcae9 --- /dev/null +++ b/brainpy/integrators/sde/euler_and_milstein.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- + +from brainpy import backend +from brainpy import profile +from brainpy.integrators import constants +from . import common + +__all__ = [ + 'euler', + 'milstein', +] + + +def _df_and_dg(code_lines, variables, parameters): + # 1. df + # df = f(x, t, *args) + all_df = [f'{var}_df' for var in variables] + code_lines.append(f' {", ".join(all_df)} = f({", ".join(variables + parameters)})') + + # 2. dg + # dg = g(x, t, *args) + all_dg = [f'{var}_dg' for var in variables] + code_lines.append(f' {", ".join(all_dg)} = g({", ".join(variables + parameters)})') + code_lines.append(' ') + + +def _dfdt(code_lines, variables, vdt): + for var in variables: + code_lines.append(f' {var}_dfdt = {var}_df * {vdt}') + code_lines.append(' ') + + +def _noise_terms(code_lines, variables): + num_vars = len(variables) + if num_vars > 1: + code_lines.append(f' all_dW = backend.normal(0.0, dt_sqrt, ({num_vars},)+backend.shape({variables[0]}_dg))') + for i, var in enumerate(variables): + code_lines.append(f' {var}_dW = all_dW[{i}]') + else: + var = variables[0] + code_lines.append(f' {var}_dW = backend.normal(0.0, dt_sqrt, backend.shape({var}))') + code_lines.append(' ') + + +# ---------- +# Wrapper +# ---------- + + +def _wrap(wrapper, f, g, dt, sde_type, var_type, wiener_type, show_code): + """The base function to format a SRK method. + + Parameters + ---------- + f : callable + The drift function of the SDE. + g : callable + The diffusion function of the SDE. + dt : float + The numerical precision. + sde_type : str + "utils.ITO_SDE" : Ito's Stochastic Calculus. + "utils.STRA_SDE" : Stratonovich's Stochastic Calculus. + wiener_type : str + var_type : str + "scalar" : with the shape of (). + "population" : with the shape of (N,) or (N1, N2) or (N1, N2, ...). + "system": with the shape of (d, ), (d, N), or (d, N1, N2). + show_code : bool + Whether show the formatted code. + + Returns + ------- + numerical_func : callable + The numerical function. + """ + + sde_type = constants.ITO_SDE if sde_type is None else sde_type + assert sde_type in constants.SUPPORTED_SDE_TYPE, f'Currently, BrainPy only support SDE types: ' \ + f'{constants.SUPPORTED_SDE_TYPE}. But we got {sde_type}.' + + var_type = constants.POPU_VAR if var_type is None else var_type + assert var_type in constants.SUPPORTED_VAR_TYPE, f'Currently, BrainPy only supports variable types: ' \ + f'{constants.SUPPORTED_VAR_TYPE}. But we got {var_type}.' + + wiener_type = constants.SCALAR_WIENER if wiener_type is None else wiener_type + assert wiener_type in constants.SUPPORTED_WIENER_TYPE, f'Currently, BrainPy only supports Wiener ' \ + f'Process types: {constants.SUPPORTED_WIENER_TYPE}. ' \ + f'But we got {wiener_type}.' + + show_code = False if show_code is None else show_code + dt = profile.get_dt() if dt is None else dt + + if f is not None and g is not None: + return wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type) + + elif f is not None: + return lambda g: wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type) + + elif g is not None: + return lambda f: wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type) + + else: + raise ValueError('Must provide "f" or "g".') + + +def _euler_wrapper(f, g, dt, sde_type, var_type, wiener_type, show_code): + vdt, variables, parameters, arguments, func_name = common.basic_info(f=f, g=g) + + # 1. code scope + code_scope = {'f': f, 'g': g, vdt: dt, f'{vdt}_sqrt': dt ** 0.5, 'backend': backend} + + # 2. code lines + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + + # 2.1 df, dg + _df_and_dg(code_lines, variables, parameters) + + # 2.2 dfdt + _dfdt(code_lines, variables, vdt) + + # 2.3 dW + _noise_terms(code_lines, variables) + + # 2.3 dgdW + # ---- + # SCALAR_WIENER : dg * dW + # VECTOR_WIENER : backend.sum(dg * dW, axis=-1) + + if wiener_type == constants.SCALAR_WIENER: + for var in variables: + code_lines.append(f' {var}_dgdW = {var}_dg * {var}_dW') + else: + for var in variables: + code_lines.append(f' {var}_dgdW = backend.sum({var}_dg * {var}_dW, axis=-1)') + code_lines.append(' ') + + if sde_type == constants.ITO_SDE: + # 2.4 new var + # ---- + # y = x + dfdt + dgdW + for var in variables: + code_lines.append(f' {var}_new = {var} + {var}_dfdt + {var}_dgdW') + code_lines.append(' ') + + elif sde_type == constants.STRA_SDE: + # 2.4 y_bar = x + backend.sum(dgdW, axis=-1) + all_bar = [f'{var}_bar' for var in variables] + for var in variables: + code_lines.append(f' {var}_bar = {var} + {var}_dgdW') + code_lines.append(' ') + + # 2.5 dg_bar = g(y_bar, t, *args) + all_dg_bar = [f'{var}_dg_bar' for var in variables] + code_lines.append(f' {", ".join(all_dg_bar)} = g({", ".join(all_bar + parameters)})') + + # 2.6 dgdW2 + # ---- + # SCALAR_WIENER : dgdW2 = dg_bar * dW + # VECTOR_WIENER : dgdW2 = backend.sum(dg_bar * dW, axis=-1) + if wiener_type == constants.SCALAR_WIENER: + for var in variables: + code_lines.append(f' {var}_dgdW2 = {var}_dg_bar * {var}_dW') + else: + for var in variables: + code_lines.append(f' {var}_dgdW2 = backend.sum({var}_dg_bar * {var}_dW, axis=-1)') + code_lines.append(' ') + + # 2.7 new var + # ---- + # y = x + dfdt + 0.5 * (dgdW + dgdW2) + for var in variables: + code_lines.append(f' {var}_new = {var} + {var}_dfdt + 0.5 * ({var}_dgdW + {var}_dgdW2)') + code_lines.append(' ') + else: + raise ValueError(f'Unknown SDE type: {sde_type}. We only ' + f'supports {constants.SUPPORTED_SDE_TYPE}.') + + # return and compile + common.return_and_compile(code_lines, code_scope, show_code, variables) + return code_scope[func_name] + + +def _milstein_wrapper(f, g, dt, sde_type, var_type, wiener_type, show_code): + vdt, variables, parameters, arguments, func_name = common.basic_info(f=f, g=g) + + # 1. code scope + code_scope = {'f': f, 'g': g, vdt: dt, f'{vdt}_sqrt': dt ** 0.5, 'backend': backend} + + # 2. code lines + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + + # 2.1 df, dg + _df_and_dg(code_lines, variables, parameters) + + # 2.2 dfdt + _dfdt(code_lines, variables, vdt) + + # 2.3 dW + _noise_terms(code_lines, variables) + + # 2.3 dgdW + # ---- + # dg * dW + for var in variables: + code_lines.append(f' {var}_dgdW = {var}_dg * {var}_dW') + code_lines.append(' ') + + # 2.4 df_bar = x + dfdt + backend.sum(dg * dt_sqrt, axis=-1) + all_df_bar = [f'{var}_df_bar' for var in variables] + if wiener_type == constants.SCALAR_WIENER: + for var in variables: + code_lines.append(f' {var}_df_bar = {var} + {var}_dfdt + {var}_dg * {vdt}_sqrt') + else: + for var in variables: + code_lines.append(f' {var}_df_bar = {var} + {var}_dfdt + backend.sum(' + f'{var}_dg * {vdt}_sqrt, axis=-1)') + + # 2.5 dg_bar = g(y_bar, t, *args) + all_dg_bar = [f'{var}_dg_bar' for var in variables] + code_lines.append(f' {", ".join(all_dg_bar)} = g({", ".join(all_df_bar + parameters)})') + code_lines.append(' ') + + # 2.6 dgdW2 + # ---- + # dgdW2 = 0.5 * (dg_bar - dg) * (dW * dW / dt_sqrt - dt_sqrt) + if sde_type == constants.ITO_SDE: + for var in variables: + code_lines.append(f' {var}_dgdW2 = 0.5 * ({var}_dg_bar - {var}_dg) * ' + f'({var}_dW * {var}_dW / {vdt}_sqrt - {vdt}_sqrt)') + elif sde_type == constants.STRA_SDE: + for var in variables: + code_lines.append(f' {var}_dgdW2 = 0.5 * ({var}_dg_bar - {var}_dg) * ' + f'{var}_dW * {var}_dW / {vdt}_sqrt') + else: + raise ValueError(f'Unknown SDE type: {sde_type}') + code_lines.append(' ') + + # 2.7 new var + # ---- + # SCALAR_WIENER : y = x + dfdt + dgdW + dgdW2 + # VECTOR_WIENER : y = x + dfdt + backend.sum(dgdW + dgdW2, axis=-1) + if wiener_type == constants.SCALAR_WIENER: + for var in variables: + code_lines.append(f' {var}_new = {var} + {var}_dfdt + {var}_dgdW + {var}_dgdW2') + elif wiener_type == constants.VECTOR_WIENER: + for var in variables: + code_lines.append(f' {var}_new = {var} + {var}_dfdt +backend.sum({var}_dgdW + {var}_dgdW2, axis=-1)') + else: + raise ValueError(f'Unknown Wiener Process : {wiener_type}') + code_lines.append(' ') + + # return and compile + common.return_and_compile(code_lines, code_scope, show_code, variables) + return code_scope[func_name] + + +# ------------------ +# Numerical methods +# ------------------ + + +def euler(f=None, g=None, dt=None, sde_type=None, var_type=None, wiener_type=None, show_code=None): + return _wrap(_euler_wrapper, f=f, g=g, dt=dt, sde_type=sde_type, var_type=var_type, + wiener_type=wiener_type, show_code=show_code) + + +def milstein(f=None, g=None, dt=None, sde_type=None, var_type=None, wiener_type=None, show_code=None): + return _wrap(_milstein_wrapper, f=f, g=g, dt=dt, sde_type=sde_type, var_type=var_type, + wiener_type=wiener_type, show_code=show_code) diff --git a/brainpy/integrators/sde/exp_euler.py b/brainpy/integrators/sde/exp_euler.py new file mode 100644 index 00000000..f66be932 --- /dev/null +++ b/brainpy/integrators/sde/exp_euler.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import sympy + +from brainpy.integrators import ast_analysis +from brainpy import backend +from brainpy import errors +from brainpy import profile +from brainpy import tools + +__all__ = [ + 'exponential_euler', +] + + +class Integrator(object): + def __init__(self, diff_eq): + if not isinstance(diff_eq, ast_analysis.DiffEquation): + if diff_eq.__class__.__name__ != 'function': + raise errors.IntegratorError('"diff_eq" must be a function or an instance of DiffEquation .') + else: + diff_eq = ast_analysis.DiffEquation(func=diff_eq) + self.diff_eq = diff_eq + self._update_code = None + self._update_func = None + + def __call__(self, y0, t, *args): + return self._update_func(y0, t, *args) + + def _compile(self): + # function arguments + func_args = ', '.join([f'_{arg}' for arg in self.diff_eq.func_args]) + + # function codes + func_code = f'def {self.py_func_name}({func_args}): \n' + func_code += tools.indent(self._update_code + '\n' + f'return _res') + tools.NoiseHandler.normal_pattern.sub( + tools.NoiseHandler.vector_replace_f, func_code) + + # function scope + code_scopes = {'numpy': np} + for k_, v_ in self.code_scope.items(): + if profile.is_jit() and callable(v_): + v_ = tools.numba_func(v_) + code_scopes[k_] = v_ + code_scopes.update(ast_analysis.get_mapping_scope()) + code_scopes['_normal_like_'] = backend.normal_like + + # function compilation + exec(compile(func_code, '', 'exec'), code_scopes) + func = code_scopes[self.py_func_name] + if profile.is_jit(): + func = tools.jit(func) + self._update_func = func + + @staticmethod + def get_integral_step(diff_eq, *args): + raise NotImplementedError + + @property + def py_func_name(self): + return self.diff_eq.func_name + + @property + def update_code(self): + return self._update_code + + @property + def update_func(self): + return self._update_func + + @property + def code_scope(self): + scope = self.diff_eq.func_scope + if profile.run_on_cpu(): + scope['_normal_like_'] = backend.normal_like + return scope + + +class ExponentialEuler(Integrator): + """First order, explicit exponential Euler method. + + For an ODE equation of the form + + .. math:: + + y^{\\prime}=f(y), \quad y(0)=y_{0} + + its schema is given by + + .. math:: + + y_{n+1}= y_{n}+h \\varphi(hA) f (y_{n}) + + where :math:`A=f^{\prime}(y_{n})` and :math:`\\varphi(z)=\\frac{e^{z}-1}{z}`. + + For linear ODE system: :math:`y^{\\prime} = Ay + B`, + the above equation is equal to + + .. math:: + + y_{n+1}= y_{n}e^{hA}-B/A(1-e^{hA}) + + For a SDE equation of the form + + .. math:: + + d y=(Ay+ F(y))dt + g(y)dW(t) = f(y)dt + g(y)dW(t), \\quad y(0)=y_{0} + + its schema is given by [16]_ + + .. math:: + + y_{n+1} & =e^{\\Delta t A}(y_{n}+ g(y_n)\\Delta W_{n})+\\varphi(\\Delta t A) F(y_{n}) \\Delta t \\\\ + &= y_n + \\Delta t \\varphi(\\Delta t A) f(y) + e^{\\Delta t A}g(y_n)\\Delta W_{n} + + where :math:`\\varphi(z)=\\frac{e^{z}-1}{z}`. + + Parameters + ---------- + diff_eq : DiffEquation + The differential equation. + + Returns + ------- + func : callable + The one-step numerical integrator function. + + References + ---------- + .. [1] Erdoğan, Utku, and Gabriel J. Lord. "A new class of exponential integrators for stochastic + differential equations with multiplicative noise." arXiv preprint arXiv:1608.07096 (2016). + """ + + def __init__(self, diff_eq): + super(ExponentialEuler, self).__init__(diff_eq) + self._update_code = self.get_integral_step(diff_eq) + self._compile() + + @staticmethod + def get_integral_step(diff_eq, *args): + dt = profile.get_dt() + f_expressions = diff_eq.get_f_expressions(substitute_vars=diff_eq.var_name) + + # code lines + code_lines = [str(expr) for expr in f_expressions[:-1]] + + # get the linear system using sympy + f_res = f_expressions[-1] + df_expr = ast_analysis.str2sympy(f_res.code).expr.expand() + s_df = sympy.Symbol(f"{f_res.var_name}") + code_lines.append(f'{s_df.name} = {ast_analysis.sympy2str(df_expr)}') + var = sympy.Symbol(diff_eq.var_name, real=True) + + # get df part + s_linear = sympy.Symbol(f'_{diff_eq.var_name}_linear') + s_linear_exp = sympy.Symbol(f'_{diff_eq.var_name}_linear_exp') + s_df_part = sympy.Symbol(f'_{diff_eq.var_name}_df_part') + if df_expr.has(var): + # linear + linear = sympy.collect(df_expr, var, evaluate=False)[var] + code_lines.append(f'{s_linear.name} = {ast_analysis.sympy2str(linear)}') + # linear exponential + linear_exp = sympy.exp(linear * dt) + code_lines.append(f'{s_linear_exp.name} = {ast_analysis.sympy2str(linear_exp)}') + # df part + df_part = (s_linear_exp - 1) / s_linear * s_df + code_lines.append(f'{s_df_part.name} = {ast_analysis.sympy2str(df_part)}') + + else: + # linear exponential + code_lines.append(f'{s_linear_exp.name} = sqrt({dt})') + # df part + code_lines.append(f'{s_df_part.name} = {ast_analysis.sympy2str(dt * s_df)}') + + # get dg part + if diff_eq.is_stochastic: + # dW + noise = f'_normal_like_({diff_eq.var_name})' + code_lines.append(f'_{diff_eq.var_name}_dW = {noise}') + # expressions of the stochastic part + g_expressions = diff_eq.get_g_expressions() + code_lines.extend([str(expr) for expr in g_expressions[:-1]]) + g_expr = g_expressions[-1].code + # get the dg_part + s_dg_part = sympy.Symbol(f'_{diff_eq.var_name}_dg_part') + code_lines.append(f'_{diff_eq.var_name}_dg_part = {g_expr} * _{diff_eq.var_name}_dW') + else: + s_dg_part = 0 + + # update expression + update = var + s_df_part + s_dg_part * s_linear_exp + + # The actual update step + code_lines.append(f'{diff_eq.var_name} = {ast_analysis.sympy2str(update)}') + return_expr = ', '.join([diff_eq.var_name] + diff_eq.return_intermediates) + code_lines.append(f'_res = {return_expr}') + + # final + code = '\n'.join(code_lines) + subs_dict = {arg: f'_{arg}' for arg in diff_eq.func_args + diff_eq.expr_names} + code = tools.word_replace(code, subs_dict) + return code + + +def exponential_euler(f): + dt = profile.get_dt() + dt_sqrt = dt ** 0.5 + + def int_f(x, t, *args): + df, linear_part, g = f(x, t, *args) + dW = backend.normal(0., 1., backend.shape(x)) + dg = dt_sqrt * g * dW + exp = backend.exp(linear_part * dt) + y1 = x + (exp - 1) / linear_part * df + exp * dg + return y1 + + return int_f diff --git a/brainpy/integrators/sde/srk_scalar.py b/brainpy/integrators/sde/srk_scalar.py new file mode 100644 index 00000000..8be78b8d --- /dev/null +++ b/brainpy/integrators/sde/srk_scalar.py @@ -0,0 +1,437 @@ +# -*- coding: utf-8 -*- + +from brainpy import backend +from brainpy import profile +from brainpy.integrators import constants +from . import common + +__all__ = [ + 'srk1w1_scalar', + 'srk2w1_scalar', + 'KlPl_scalar', +] + + +# ------- +# Helpers +# ------- + + +def _noise_terms(code_lines, variables, vdt, triple_integral=True): + num_vars = len(variables) + if num_vars > 1: + code_lines.append(f' all_I1 = backend.normal(0.0, dt_sqrt, ({num_vars},)+backend.shape({variables[0]}))') + code_lines.append(f' all_I0 = backend.normal(0.0, dt_sqrt, ({num_vars},)+backend.shape({variables[0]}))') + code_lines.append(f' all_I10 = 0.5 * {vdt} * (all_I1 + all_I0 / 3.0 ** 0.5)') + code_lines.append(f' all_I11 = 0.5 * (all_I1 ** 2 - {vdt})') + if triple_integral: + code_lines.append(f' all_I111 = (all_I1 ** 3 - 3 * {vdt} * all_I1) / 6') + code_lines.append(f' ') + for i, var in enumerate(variables): + code_lines.append(f' {var}_I1 = all_I1[{i}]') + code_lines.append(f' {var}_I0 = all_I0[{i}]') + code_lines.append(f' {var}_I10 = all_I10[{i}]') + code_lines.append(f' {var}_I11 = all_I11[{i}]') + if triple_integral: + code_lines.append(f' {var}_I111 = all_I111[{i}]') + code_lines.append(f' ') + else: + var = variables[0] + code_lines.append(f' {var}_I1 = backend.normal(0.0, dt_sqrt, backend.shape({var}))') + code_lines.append(f' {var}_I0 = backend.normal(0.0, dt_sqrt, backend.shape({var}))') + code_lines.append(f' {var}_I10 = 0.5 * {vdt} * ({var}_I1 + {var}_I0 / 3.0 ** 0.5)') + code_lines.append(f' {var}_I11 = 0.5 * ({var}_I1 ** 2 - {vdt})') + if triple_integral: + code_lines.append(f' {var}_I111 = ({var}_I1 ** 3 - 3 * {vdt} * {var}_I1) / 6') + code_lines.append(' ') + + +def _state1(code_lines, variables, parameters): + f_names = [f'{var}_f_H0s1' for var in variables] + g_names = [f'{var}_g_H1s1' for var in variables] + code_lines.append(f' {", ".join(f_names)} = f({", ".join(variables + parameters)})') + code_lines.append(f' {", ".join(g_names)} = g({", ".join(variables + parameters)})') + code_lines.append(' ') + + +# --------- +# Wrappers +# --------- + + +def _srk1w1_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): + vdt, variables, parameters, arguments, func_name = common.basic_info(f=f, g=g) + + # 1. code scope + code_scope = {'f': f, 'g': g, vdt: dt, f'{vdt}_sqrt': dt ** 0.5, 'backend': backend} + + # 2. code lines + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + + # 2.1 noise + _noise_terms(code_lines, variables, vdt, triple_integral=True) + + # 2.2 stage 1 + _state1(code_lines, variables, parameters) + + # 2.3 stage 2 + all_H0s2, all_H1s2 = [], [] + for var in variables: + code_lines.append(f' {var}_H0s2 = {var} + {vdt} * 0.75 * {var}_f_H0s1 + ' + f'1.5 * {var}_g_H1s1 * {var}_I10 / {vdt}') + all_H0s2.append(f'{var}_H0s2') + code_lines.append(f' {var}_H1s2 = {var} + {vdt} * 0.25 * {var}_f_H0s1 + ' + f'dt_sqrt * 0.5 * {var}_g_H1s1') + all_H1s2.append(f'{var}_H1s2') + all_H0s2.append(f't + 0.75 * {vdt}') # t + all_H1s2.append(f't + 0.25 * {vdt}') # t + f_names = [f'{var}_f_H0s2' for var in variables] + code_lines.append(f' {", ".join(f_names)} = f({", ".join(all_H0s2 + parameters[1:])})') + g_names = [f'{var}_g_H1s2' for var in variables] + code_lines.append(f' {", ".join(g_names)} = g({", ".join(all_H1s2 + parameters[1:])})') + code_lines.append(' ') + + # 2.4 state 3 + all_H1s3 = [] + for var in variables: + code_lines.append(f' {var}_H1s3 = {var} + {vdt} * {var}_f_H0s1 - dt_sqrt * {var}_g_H1s1') + all_H1s3.append(f'{var}_H1s3') + all_H1s3.append(f't + {vdt}') # t + g_names = [f'{var}_g_H1s3' for var in variables] + code_lines.append(f' {", ".join(g_names)} = g({", ".join(all_H1s3 + parameters[1:])})') + code_lines.append(' ') + + # 2.5 state 4 + all_H1s4 = [] + for var in variables: + code_lines.append(f' {var}_H1s4 = {var} + 0.25 * {vdt} * {var}_f_H0s1 + dt_sqrt * ' + f'(-5 * {var}_g_H1s1 + 3 * {var}_g_H1s2 + 0.5 * {var}_g_H1s3)') + all_H1s4.append(f'{var}_H1s4') + all_H1s4.append(f't + 0.25 * {vdt}') # t + g_names = [f'{var}_g_H1s4' for var in variables] + code_lines.append(f' {", ".join(g_names)} = g({", ".join(all_H1s4 + parameters[1:])})') + code_lines.append(' ') + + # 2.6 final stage + for var in variables: + code_lines.append(f' {var}_f1 = {var}_f_H0s1/3 + {var}_f_H0s2 * 2/3') + code_lines.append(f' {var}_g1 = -{var}_I1 - {var}_I11/dt_sqrt + 2 * {var}_I10/{vdt} - 2 * {var}_I111/{vdt}') + code_lines.append(f' {var}_g2 = {var}_I1 * 4/3 + {var}_I11 / dt_sqrt * 4/3 - ' + f'{var}_I10 / {vdt} * 4/3 + {var}_I111 / {vdt} * 5/3') + code_lines.append(f' {var}_g3 = {var}_I1 * 2/3 - {var}_I11/dt_sqrt/3 - ' + f'{var}_I10 / {vdt} * 2/3 - {var}_I111 / {vdt} * 2/3') + code_lines.append(f' {var}_g4 = {var}_I111 / {vdt}') + code_lines.append(f' {var}_new = {var} + {vdt} * {var}_f1 + {var}_g1 * {var}_g_H1s1 + ' + f'{var}_g2 * {var}_g_H1s2 + {var}_g3 * {var}_g_H1s3 + {var}_g4 * {var}_g_H1s4') + code_lines.append(' ') + + # return and compile + common.return_and_compile(code_lines, code_scope, show_code, variables) + return code_scope[func_name] + + +def _srk2w1_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): + vdt, variables, parameters, arguments, func_name = common.basic_info(f=f, g=g) + + # 1. code scope + code_scope = {'f': f, 'g': g, vdt: dt, f'{vdt}_sqrt': dt ** 0.5, 'backend': backend} + + # 2. code lines + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + + # 2.1 noise + _noise_terms(code_lines, variables, vdt, triple_integral=True) + + # 2.2 stage 1 + _state1(code_lines, variables, parameters) + + # 2.3 stage 2 + # ---- + # H0s2 = x + dt * f_H0s1 + # H1s2 = x + dt * 0.25 * f_H0s1 - dt_sqrt * 0.5 * g_H1s1 + # f_H0s2 = f(H0s2, t + dt, *args) + # g_H1s2 = g(H1s2, t + 0.25 * dt, *args) + all_H0s2, all_H1s2 = [], [] + for var in variables: + code_lines.append(f' {var}_H0s2 = {var} + {vdt} * {var}_f_H0s1') + all_H0s2.append(f'{var}_H0s2') + code_lines.append(f' {var}_H1s2 = {var} + {vdt} * 0.25 * {var}_f_H0s1 - ' + f'dt_sqrt * 0.5 * {var}_g_H1s1') + all_H1s2.append(f'{var}_H1s2') + all_H0s2.append(f't + {vdt}') # t + all_H1s2.append(f't + 0.25 * {vdt}') # t + f_names = [f'{var}_f_H0s2' for var in variables] + code_lines.append(f' {", ".join(f_names)} = f({", ".join(all_H0s2 + parameters[1:])})') + g_names = [f'{var}_g_H1s2' for var in variables] + code_lines.append(f' {", ".join(g_names)} = g({", ".join(all_H1s2 + parameters[1:])})') + code_lines.append(' ') + + # 2.4 state 3 + # --- + # H0s3 = x + dt * (0.25 * f_H0s1 + 0.25 * f_H0s2) + (g_H1s1 + 0.5 * g_H1s2) * I10 / dt + # H1s3 = x + dt * f_H0s1 + dt_sqrt * g_H1s1 + # f_H0s3 = g(H0s3, t + 0.5 * dt, *args) + # g_H1s3 = g(H1s3, t + dt, *args) + all_H0s3, all_H1s3 = [], [] + for var in variables: + code_lines.append(f' {var}_H0s3 = {var} + {vdt} * (0.25 * {var}_f_H0s1 + 0.25 * {var}_f_H0s2) + ' + f'({var}_g_H1s1 + 0.5 * {var}_g_H1s2) * {var}_I10 / {vdt}') + all_H0s3.append(f'{var}_H0s3') + code_lines.append(f' {var}_H1s3 = {var} + {vdt} * {var}_f_H0s1 + dt_sqrt * {var}_g_H1s1') + all_H1s3.append(f'{var}_H1s3') + all_H0s3.append(f't + 0.5 * {vdt}') # t + all_H1s3.append(f't + {vdt}') # t + f_names = [f'{var}_f_H0s3' for var in variables] + g_names = [f'{var}_g_H1s3' for var in variables] + code_lines.append(f' {", ".join(f_names)} = f({", ".join(all_H0s3 + parameters[1:])})') + code_lines.append(f' {", ".join(g_names)} = g({", ".join(all_H1s3 + parameters[1:])})') + code_lines.append(' ') + + # 2.5 state 4 + # ---- + # H1s4 = x + dt * 0.25 * f_H0s3 + dt_sqrt * (2 * g_H1s1 - g_H1s2 + 0.5 * g_H1s3) + # g_H1s4 = g(H1s4, t + 0.25 * dt, *args) + all_H1s4 = [] + for var in variables: + code_lines.append(f' {var}_H1s4 = {var} + 0.25 * {vdt} * {var}_f_H0s1 + dt_sqrt * ' + f'(2 * {var}_g_H1s1 - {var}_g_H1s2 + 0.5 * {var}_g_H1s3)') + all_H1s4.append(f'{var}_H1s4') + all_H1s4.append(f't + 0.25 * {vdt}') # t + g_names = [f'{var}_g_H1s4' for var in variables] + code_lines.append(f' {", ".join(g_names)} = g({", ".join(all_H1s4 + parameters[1:])})') + code_lines.append(' ') + + # 2.6 final stage + # ---- + # f1 = f_H0s1 / 6 + f_H0s2 / 6 + f_H0s3 * 2 / 3 + # g1 = - I1 + I11 / dt_sqrt + 2 * I10 / dt - 2 * I111 / dt + # g2 = I1 * 4 / 3 - I11 / dt_sqrt * 4 / 3 - I10 / dt * 4 / 3 + I111 / dt * 5 / 3 + # g3 = I1 * 2 / 3 + I11 / dt_sqrt / 3 - I10 / dt * 2 / 3 - I111 / dt * 2 / 3 + # g4 = I111 / dt + # y1 = x + dt * f1 + g1 * g_H1s1 + g2 * g_H1s2 + g3 * g_H1s3 + g4 * g_H1s4 + for var in variables: + code_lines.append(f' {var}_f1 = {var}_f_H0s1/6 + {var}_f_H0s2/6 + {var}_f_H0s3*2/3') + code_lines.append(f' {var}_g1 = -{var}_I1 + {var}_I11/dt_sqrt + 2 * {var}_I10/{vdt} - 2 * {var}_I111/{vdt}') + code_lines.append(f' {var}_g2 = {var}_I1 * 4/3 - {var}_I11 / dt_sqrt * 4/3 - ' + f'{var}_I10 / {vdt} * 4/3 + {var}_I111 / {vdt} * 5/3') + code_lines.append(f' {var}_g3 = {var}_I1 * 2/3 + {var}_I11/dt_sqrt/3 - ' + f'{var}_I10 / {vdt} * 2/3 - {var}_I111 / {vdt} * 2/3') + code_lines.append(f' {var}_g4 = {var}_I111 / {vdt}') + code_lines.append(f' {var}_new = {var} + {vdt} * {var}_f1 + {var}_g1 * {var}_g_H1s1 + ' + f'{var}_g2 * {var}_g_H1s2 + {var}_g3 * {var}_g_H1s3 + {var}_g4 * {var}_g_H1s4') + code_lines.append(' ') + + # return and compile + common.return_and_compile(code_lines, code_scope, show_code, variables) + return code_scope[func_name] + + +def _KlPl_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): + vdt, variables, parameters, arguments, func_name = common.basic_info(f=f, g=g) + + # 1. code scope + code_scope = {'f': f, 'g': g, vdt: dt, f'{vdt}_sqrt': dt ** 0.5, 'backend': backend} + + # 2. code lines + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + + # 2.1 noise + _noise_terms(code_lines, variables, vdt, triple_integral=False) + + # 2.2 stage 1 + _state1(code_lines, variables, parameters) + + # 2.3 stage 2 + # ---- + # H1s2 = x + dt * f_H0s1 + dt_sqrt * g_H1s1 + # g_H1s2 = g(H1s2, t0, *args) + all_H1s2 = [] + for var in variables: + code_lines.append(f' {var}_H1s2 = {var} + {vdt} * {var}_f_H0s1 + dt_sqrt * {var}_g_H1s1') + all_H1s2.append(f'{var}_H1s2') + g_names = [f'{var}_g_H1s2' for var in variables] + code_lines.append(f' {", ".join(g_names)} = g({", ".join(all_H1s2 + parameters)})') + code_lines.append(' ') + + # 2.4 final stage + # ---- + # g1 = (I1 - I11 / dt_sqrt + I10 / dt) + # g2 = I11 / dt_sqrt + # y1 = x + dt * f_H0s1 + g1 * g_H1s1 + g2 * g_H1s2 + for var in variables: + code_lines.append(f' {var}_g1 = -{var}_I1 + {var}_I11/dt_sqrt + {var}_I10/{vdt}') + code_lines.append(f' {var}_g2 = {var}_I11 / dt_sqrt') + code_lines.append(f' {var}_new = {var} + {vdt} * {var}_f_H0s1 + ' + f'{var}_g1 * {var}_g_H1s1 + {var}_g2 * {var}_g_H1s2') + code_lines.append(' ') + + # return and compile + common.return_and_compile(code_lines, code_scope, show_code, variables) + return code_scope[func_name] + + +def _wrap(wrapper, f, g, dt, sde_type, var_type, wiener_type, show_code): + """The base function to format a SRK method. + + Parameters + ---------- + f : callable + The drift function of the SDE. + g : callable + The diffusion function of the SDE. + dt : float + The numerical precision. + sde_type : str + "utils.ITO_SDE" : Ito's Stochastic Calculus. + "utils.STRA_SDE" : Stratonovich's Stochastic Calculus. + wiener_type : str + var_type : str + "scalar" : with the shape of (). + "population" : with the shape of (N,) or (N1, N2) or (N1, N2, ...). + "system": with the shape of (d, ), (d, N), or (d, N1, N2). + show_code : bool + Whether show the formatted code. + + Returns + ------- + numerical_func : callable + The numerical function. + """ + + var_type = constants.POPU_VAR if var_type is None else var_type + assert var_type in constants.SUPPORTED_VAR_TYPE, f'Currently, BrainPy only supports variable types: ' \ + f'{constants.SUPPORTED_VAR_TYPE}. But we got {var_type}.' + + sde_type = constants.ITO_SDE if sde_type is None else sde_type + assert sde_type == constants.ITO_SDE, 'SRK method for SDEs with scalar noise only supports Ito SDE type.' + + assert wiener_type == constants.SCALAR_WIENER, 'SRK method for SDEs with scalar noise only supports ' \ + 'scalar Wiener Process.' + + show_code = False if show_code is None else show_code + dt = profile.get_dt() if dt is None else dt + + if f is not None and g is not None: + return wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type) + + elif f is not None: + return lambda g: wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type) + + elif g is not None: + return lambda f: wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type) + + else: + raise ValueError('Must provide "f" or "g".') + + +# ------------------- +# Numerical functions +# ------------------- + + +def srk1w1_scalar(f=None, g=None, dt=None, sde_type=None, var_type=None, wiener_type=None, show_code=None): + """Order 2.0 weak SRK methods for SDEs with scalar Wiener process. + + This method has have strong orders :math:`(p_d, p_s) = (2.0,1.5)`. + + The Butcher table is: + + .. math:: + + \\begin{array}{l|llll|llll|llll} + 0 &&&&& &&&& &&&& \\\\ + 3/4 &3/4&&&& 3/2&&& &&&& \\\\ + 0 &0&0&0&& 0&0&0&& &&&&\\\\ + \\hline + 0 \\\\ + 1/4 & 1/4&&& & 1/2&&&\\\\ + 1 & 1&0&&& -1&0&\\\\ + 1/4& 0&0&1/4&& -5&3&1/2\\\\ + \\hline + & 1/3& 2/3& 0 & 0 & -1 & 4/3 & 2/3&0 & -1 &4/3 &-1/3 &0 \\\\ + \\hline + & &&&& 2 &-4/3 & -2/3 & 0 & -2 & 5/3 & -2/3 & 1 + \\end{array} + + + References + ---------- + + .. [1] Rößler, Andreas. "Strong and weak approximation methods for stochastic differential + equations—some recent developments." Recent developments in applied probability and + statistics. Physica-Verlag HD, 2010. 127-153. + .. [2] Rößler, Andreas. "Runge–Kutta methods for the strong approximation of solutions of + stochastic differential equations." SIAM Journal on Numerical Analysis 48.3 + (2010): 922-952. + + """ + return _wrap(_srk1w1_wrapper, f=f, g=g, dt=dt, sde_type=sde_type, var_type=var_type, + wiener_type=wiener_type, show_code=show_code) + + +def srk2w1_scalar(f=None, g=None, dt=None, sde_type=None, var_type=None, wiener_type=None, show_code=None): + """Order 1.5 Strong SRK Methods for SDEs witdt Scalar Noise. + + This method has have strong orders :math:`(p_d, p_s) = (3.0,1.5)`. + + The Butcher table is: + + .. math:: + + \\begin{array}{c|cccc|cccc|ccc|} + 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & & & & \\\\ + 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & & & & \\\\ + 1 / 2 & 1 / 4 & 1 / 4 & 0 & 0 & 1 & 1 / 2 & 0 & 0 & & & & \\\\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & & & & \\\\ + \\hline 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & & & & \\\\ + 1 / 4 & 1 / 4 & 0 & 0 & 0 & -1 / 2 & 0 & 0 & 0 & & & & \\\\ + 1 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & & & & \\\\ + 1 / 4 & 0 & 0 & 1 / 4 & 0 & 2 & -1 & 1 / 2 & 0 & & & & \\\\ + \\hline & 1 / 6 & 1 / 6 & 2 / 3 & 0 & -1 & 4 / 3 & 2 / 3 & 0 & -1 & -4 / 3 & 1 / 3 & 0 \\\\ + \\hline & & & & &2 & -4 / 3 & -2 / 3 & 0 & -2 & 5 / 3 & -2 / 3 & 1 + \\end{array} + + + References + ---------- + + [1] Rößler, Andreas. "Strong and weak approximation methods for stochastic differential + equations—some recent developments." Recent developments in applied probability and + statistics. Physica-Verlag HD, 2010. 127-153. + [2] Rößler, Andreas. "Runge–Kutta methods for the strong approximation of solutions of + stochastic differential equations." SIAM Journal on Numerical Analysis 48.3 + (2010): 922-952. + """ + return _wrap(_srk2w1_wrapper, f=f, g=g, dt=dt, sde_type=sde_type, var_type=var_type, + wiener_type=wiener_type, show_code=show_code) + + +def KlPl_scalar(f=None, g=None, dt=None, sde_type=None, var_type=None, wiener_type=None, show_code=None): + """Order 1.0 Strong SRK Methods for SDEs with Scalar Noise. + + This method has have orders :math:`p_s = 1.0`. + + The Butcher table is: + + .. math:: + + \\begin{array}{c|cc|cc|cc|c} + 0 & 0 & 0 & 0 & 0 & & \\\\ + 0 & 0 & 0 & 0 & 0 & & \\\\ + \\hline 0 & 0 & 0 & 0 & 0 & & \\\\ + 0 & 1 & 0 & 1 & 0 & & \\\\ + \\hline 0 & 1 & 0 & 1 & 0 & -1 & 1 \\\\ + \\hline & & & 1 & 0 & 0 & 0 + \\end{array} + + References + ---------- + + [1] P. E. Kloeden, E. Platen, Numerical Solution of Stochastic Differential + Equations, 2nd Edition, Springer, Berlin Heidelberg New York, 1995. + """ + return _wrap(_KlPl_wrapper, f=f, g=g, dt=dt, sde_type=sde_type, var_type=var_type, + wiener_type=wiener_type, show_code=show_code) diff --git a/brainpy/integrators/sde/srk_strong.py b/brainpy/integrators/sde/srk_strong.py new file mode 100644 index 00000000..7b1aa153 --- /dev/null +++ b/brainpy/integrators/sde/srk_strong.py @@ -0,0 +1,443 @@ +# -*- coding: utf-8 -*- + +from brainpy import backend +from brainpy import profile +from brainpy.integrators import constants +from . import common + +__all__ = [ + 'srk1_strong', +] + + +def _vector_wiener_terms(code_lines, sde_type, vdt, shape_D, shape_m): + if sde_type == constants.ITO_SDE: + I2 = f'0.5*(_term3 - {vdt} * backend.eye({shape_m})) + _a*0.5*{vdt}/math.pi' + elif sde_type == constants.STRA_SDE: + I2 = f'0.5*_term3 + _a*0.5*dt/math.pi' + else: + raise ValueError(f'Unknown SDE type: {sde_type}. We only supports {constants.SUPPORTED_SDE_TYPE}.') + + if shape_D: + shape_D = shape_D + '+' + + noise_string = f''' + # Noise Terms # + # ----------- # + + # single Ito integrals + _I1 = backend.normal(0., {vdt}_sqrt, {shape_D}({shape_m},)) + # double Ito integrals + _h = (2.0 / {vdt}) ** 0.5) + _a = backend.zeros(shape={shape_D}({shape_m}, {shape_m})) + for _k in range(1, num_iter + 1): + _x = backend.normal(loc=0., scale=1., size={shape_D}({shape_m}, 1)) + _y = backend.normal(loc=0., scale=1., size={shape_D}(1, {shape_m})) + _h * _I1 + _term1 = backend.matmul(_x, _y) + _term2 = backend.matmul(backend.reshape(_y, {shape_D}({shape_m}, 1)), + backend.reshape(_x, {shape_D}(1, {shape_m}))) + _a += (_term1 - _term2) / _k + _I1_rs = backend.reshape(_I1, {shape_D}({shape_m}, 1)) + _term3 = backend.matmul(_I1_rs, backend.reshape(_I1, {shape_D}(1, {shape_m}))) + _I2 = {I2} + ''' + noise_lines = noise_string.split('\n') + code_lines.extend(noise_lines) + + +# ---------- +# Wrapper +# ---------- + + +def _srk2_pop_var_vector_wiener(sde_type, code_lines, variables, parameters, vdt): + # shape information + # ----- + all_f = [f'f_{var}' for var in variables] + all_g = [f'g_{var}' for var in variables] + noise_string = f''' + {", ".join(all_f)} = f({", ".join(variables + parameters)}) # shape = (..) + {", ".join(all_g)} = g({", ".join(variables + parameters)}) # shape = (.., m) + noise_shape = backend.shape(g_x1) + _D = noise_shape[:-1] + _m = noise_shape[-1] + ''' + code_lines.extend(noise_string.split("\n")) + + # noise terms + _vector_wiener_terms(code_lines, sde_type, vdt, shape_D='_D', shape_m='_m') + + # numerical integration + # step 1 + # --- + # g_x1_rs = backend.reshape(g_x1, _D + (1, _m)) + # g_x2_rs = backend.reshape(g_x2, _D + (1, _m)) + for var in variables: + code_lines.append(f" g_{var}_rs = backend.reshape(g_{var}, _D+(1, _m))") + # step 2 + # --- + # g_H1_x1 = backend.reshape(backend.matmul(g_x1_rs, _I2) / dt_sqrt, _D + (_m,)) + # g_H1_x2 = backend.reshape(backend.matmul(g_x2_rs, _I2) / dt_sqrt, _D + (_m,)) + for var in variables: + code_lines.append(f' g_H1_{var} = backend.reshape(backend.matmul(g_{var}_rs, _I2) / {vdt}_sqrt, _D + (_m,))') + # step 3 + # --- + # x1_rs = backend.reshape(x1, _D + (1,)) + # x2_rs = backend.reshape(x2, _D + (1,)) + for var in variables: + code_lines.append(f' {var}_rs = backend.reshape({var}, _D + (1,))') + # step 4 + # --- + # H2_x1 = x1_rs + g_H1_x1 + # H3_x1 = x1_rs - g_H1_x1 + for var in variables: + code_lines.append(f' H2_{var} = {var}_rs + g_H1_{var}') + code_lines.append(f' H3_{var} = {var}_rs - g_H1_{var}') + code_lines.append(' ') + # step 5 + # --- + # _g_x1 = backend.matmul(g_x1_rs, _I1_rs) + for var in variables: + code_lines.append(f' _g_{var} = backend.matmul(g_{var}_rs, _I1_rs)') + # step 6 + # ---- + # x1_new = x1 + f_x1 + _g_x1[..., 0, 0] + for var in variables: + code_lines.append(f' {var}_new = {var} + f_{var} + _g_{var}[..., 0, 0]') + # for _k in range(_m): + code_lines.append('for _k in range(_m):') + # g_x1_H2, g_x2_H2 = g(H2_x1[..., _k], H2_x2[..., _k], t, *args) + all_H2 = [f'H2_{var}[..., _k]' for var in variables] + all_g_H2 = [f'g_{var}_H2' for var in variables] + code_lines.append(f' {", ".join(all_g_H2)} = g({", ".join(all_H2 + parameters)})') + # g_x1_H3, g_x2_H3 = g(H3_x1[..., _k], H3_x2[..., _k], t, *args) + all_H3 = [f'H3_{var}[..., _k]' for var in variables] + all_g_H3 = [f'g_{var}_H3' for var in variables] + code_lines.append(f' {", ".join(all_g_H3)} = g({", ".join(all_H3 + parameters)})') + # x1_new += 0.5 * dt_sqrt * (g_x1_H2[..., _k] - g_x1_H3[..., _k]) + # x2_new += 0.5 * dt_sqrt * (g_x2_H2[..., _k] - g_x2_H3[..., _k]) + for var in variables: + code_lines.append(f' {var}_new += 0.5 * {vdt}_sqrt * (g_{var}_H2[..., _k] - g_{var}_H3[..., _k])') + + +def _srk2_pop_or_scalar_var_scalar_wiener(sde_type, code_lines, variables, parameters, vdt): + if sde_type == constants.ITO_SDE: + I2 = f'0.5 * (_I1 * _I1 - {vdt})' + elif sde_type == constants.STRA_SDE: + I2 = f'0.5 * _I1 * _I1' + else: + raise ValueError(f'Unknown SDE type: {sde_type}. We only supports {constants.SUPPORTED_SDE_TYPE}.') + + # shape info + # ----- + all_f = [f'f_{var}' for var in variables] + all_g = [f'g_{var}' for var in variables] + + code_string = f''' + {", ".join(all_f)} = f({", ".join(variables + parameters)}) # shape = (..) + {", ".join(all_g)} = g({", ".join(variables + parameters)}) # shape = (..) + + # single Ito integrals + _I1 = backend.normal(0., {vdt}_sqrt, backend.shape({variables[0]})) # shape = (..) + # double Ito integrals + _I2 = {I2} # shape = (..) + ''' + code_splits = code_string.split('\n') + code_lines.extend(code_splits) + + # numerical integration + # ----- + # H1 + for var in variables: + code_lines.append(f' g_H1_{var} = g_{var} * _I2 / {vdt}_sqrt # shape (.., )') + # H2 + all_H2 = [f'H2_{var}' for var in variables] + for var in variables: + code_lines.append(f' H2_{var} = {var} + g_H1_{var} # shape (.., )') + all_g_H2 = [f'g_{var}_H2' for var in variables] + code_lines.append(f' {", ".join(all_g_H2)} = g({", ".join(all_H2 + parameters)})') + code_lines.append(f' ') + # H3 + all_H3 = [f'H3_{var}' for var in variables] + for var in variables: + code_lines.append(f' H3_{var} = {var} - g_H1_{var} # shape (.., )') + all_g_H3 = [f'g_{var}_H3' for var in variables] + code_lines.append(f' {", ".join(all_g_H3)} = g({", ".join(all_H3 + parameters)})') + code_lines.append(f' ') + # final results + for var in variables: + code_lines.append(f' {var}_new = {var} + f_{var} + g_{var} * _I1 ' + f'+ 0.5 * {vdt}_sqrt * (g_{var}_H2 - g_{var}_H3)') + + +def _srk1_scalar_var_with_vector_wiener(sde_type, code_lines, variables, parameters, vdt): + # shape information + all_f = [f'f_{var}' for var in variables] + all_g = [f'g_{var}' for var in variables] + code1 = f''' + # shape info # + # ---------- # + + {", ".join(all_f)} = f({", ".join(variables + parameters)}) # shape = () + {", ".join(all_g)} = g({", ".join(variables + parameters)}) # shape = (m) + noise_shape = backend.shape(g_x1) + _m = noise_shape[0] + ''' + code_lines.extend(code1.split('\n')) + + # noise term + _vector_wiener_terms(code_lines, sde_type, vdt, shape_D='', shape_m='_m') + + # numerical integration + + # p1 + # --- + # g_x1_rs = backend.reshape(g_x1, (1, _m)) + # g_x2_rs = backend.reshape(g_x2, (1, _m)) + for var in variables: + code_lines.append(f' g_{var}_rs = backend.reshape(g_{var}, (1, _m))') + + # p2 + # --- + # g_H1_x1 = backend.matmul(g_x1_rs, _I2) / dt_sqrt # shape (1, m) + # g_H1_x2 = backend.matmul(g_x2_rs, _I2) / dt_sqrt # shape (1, m) + for var in variables: + code_lines.append(f' g_H1_{var} = backend.matmul(g_{var}_rs, _I2) / {vdt}_sqrt # shape (1, m)') + + # p3 + # --- + # H2_x1 = x1 + g_H1_x1[0] # shape (m) + # H3_x1 = x1 - g_H1_x1[0] # shape (m) + for var in variables: + code_lines.append(f' H2_{var} = {var} + g_H1_{var}[0] # shape (m)') + code_lines.append(' ') + + # p4 + # --- + # g1_x1 = backend.matmul(g_x1_rs, _I1_rs) # shape (1, 1) + # x1_new = x1 + f_x1 + g1_x1[0, 0] # shape () + for var in variables: + code_lines.append(f' g1_{var} = backend.matmul(g_{var}_rs, _I1_rs) # shape (1, 1)') + code_lines.append(f' {var}_new = {var} + f_{var} + g1_{var}[0, 0] # shape ()') + + # p5 + # --- + # for _k in range(_m): + # g_x1_H2, g_x2_H2 = g(H2_x1[_k], H2_x2[_k], t, *args) + # g_x1_H3, g_x2_H3 = g(H3_x1[_k], H3_x2[_k], t, *args) + # x1_new += 0.5 * dt_sqrt * (g_x1_H2[_k] - g_x1_H3[_k]) + # x2_new += 0.5 * dt_sqrt * (g_x2_H2[_k] - g_x2_H3[_k]) + code_lines.append(' for _k in range(_m):') + all_h2_k = [f'H2_{var}[_k]' for var in variables] + all_g_h2 = [f'g_{var}_H2' for var in variables] + code_lines.append(f' {", ".join(all_g_h2)} = g({", ".join(all_h2_k + parameters)})') + all_h3_k = [f'H3_{var}[_k]' for var in variables] + all_g_h3 = [f'g_{var}_H3' for var in variables] + code_lines.append(f' {", ".join(all_g_h3)} = g({", ".join(all_h3_k + parameters)})') + for var in variables: + code_lines.append(f' {var}_new += 0.5 * {vdt}_sqrt * (g_{var}_H2[_k] - g_{var}_H3[_k])') + + +def _srk1_system_var_with_vector_wiener(sde_type, code_lines, variables, parameters, vdt): + # shape information + code1 = f''' + # shape infor # + # ----------- # + + f_x = f({", ".join(variables + parameters)}) # shape = (d, ..) + g_x = g({", ".join(variables + parameters)}) # shape = (d, .., m) + _shape = backend.shape(g_x) + _d = _shape[0] + _m = _shape[-1] + _D = _shape[1:-1] + ''' + code_lines.extend(code1.split('\n')) + + # noise term + _vector_wiener_terms(code_lines, sde_type, vdt, shape_D='_D', shape_m='_m') + + # numerical integration + code2 = f''' + # numerical integration # + # --------------------- # + + g_x2 = backend.moveaxis(g_x, 0, -2) # shape = (.., d, m) + g_H1_k = backend.matmul(g_x2, _I2) / dt_sqrt # shape (.., d, m) + g_H1_k = backend.moveaxis(g_H1_k, -2, 0) # shape (d, .., m) + x_rs = backend.reshape(x, (_d,) + _D + (1,)) + H2 = x_rs + g_H1_k # shape (d, .., m) + H3 = x_rs - g_H1_k # shape (d, .., m) + + g1 = backend.matmul(g_x2, _I1_rs) # shape (.., d, 1) + g1 = backend.moveaxis(g1, -2, 0) # shape (d, .., 1) + y = x + f_x + g1[..., 0] # shape (d, ..) + for _k in range(_m): + y += 0.5 * dt_sqrt * g(H2[..., _k], t, *args)[..., _k] + y -= 0.5 * dt_sqrt * g(H3[..., _k], t, *args)[..., _k] + ''' + code_lines.extend(code2.split('\n')) + + +def _srk1_system_var_with_scalar_wiener(sde_type, code_lines, variables, parameters, vdt): + if sde_type == constants.ITO_SDE: + I2 = f'0.5 * (_I1 * _I1 - {vdt})' + elif sde_type == constants.STRA_SDE: + I2 = f'0.5 * _I1 * _I1' + else: + raise ValueError(f'Unknown SDE type: {sde_type}. We only supports {constants.SUPPORTED_SDE_TYPE}.') + + code_string = f''' + f_x = f({", ".join(variables + parameters)}) # shape = (d, ..) + g_x = g({", ".join(variables + parameters)}) # shape = (d, ..) + _shape = backend.shape(g_x) + _d = _shape[0] + _D = _shape[1:] + + # single Ito integrals + _I1 = backend.normal(0., {vdt}_sqrt, _D) # shape = (..) + # double Ito integrals + _I2 = {I2} # shape = (..) + + # numerical integration # + # --------------------- # + g_H1_k = g_x * _I2 / {vdt}_sqrt # shape (d, ..) + H2 = x + g_H1_k # shape (d, ..) + H3 = x - g_H1_k # shape (d, ..) + + g1 = g_x * _I1 # shape (d, ..) + x_new = x + f_x + g1 # shape (d, ..) + x_new += 0.5 * {vdt}_sqrt * g(H2, {", ".join(parameters)}) + x_new -= 0.5 * {vdt}_sqrt * g(H3, {", ".join(parameters)}) + ''' + code_splits = code_string.split('\n') + code_lines.extend(code_splits) + + +def _srk1_wrapper(f, g, dt, sde_type, var_type, wiener_type, show_code, num_iter): + vdt, variables, parameters, arguments, func_name = common.basic_info(f=f, g=g) + + # 1. code scope + code_scope = {'f': f, 'g': g, vdt: dt, f'{vdt}_sqrt': dt ** 0.5, + 'backend': backend, 'num_iter': num_iter} + + # 2. code lines + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + + if var_type == constants.SYSTEM_VAR: + if len(variables) > 1: + raise ValueError(f'SDE with {constants.SYSTEM_VAR} variable type only ' + f'supports one system variable. But we got {variables}.') + + if wiener_type == constants.SCALAR_WIENER: + _srk1_system_var_with_scalar_wiener(sde_type, code_lines, variables, parameters, vdt) + elif wiener_type == constants.VECTOR_WIENER: + _srk1_system_var_with_vector_wiener(sde_type, code_lines, variables, parameters, vdt) + else: + raise ValueError(f'Unknown Wiener type: {wiener_type}, we only ' + f'supports {constants.SUPPORTED_WIENER_TYPE}') + + elif var_type == constants.SCALAR_VAR: + if wiener_type == constants.SCALAR_WIENER: + _srk2_pop_or_scalar_var_scalar_wiener(sde_type, code_lines, variables, parameters, vdt) + elif wiener_type == constants.VECTOR_WIENER: + _srk1_scalar_var_with_vector_wiener(sde_type, code_lines, variables, parameters, vdt) + else: + raise ValueError(f'Unknown Wiener type: {wiener_type}, we only ' + f'supports {constants.SUPPORTED_WIENER_TYPE}') + + elif var_type == constants.POPU_VAR: + if wiener_type == constants.SCALAR_WIENER: + _srk2_pop_or_scalar_var_scalar_wiener(sde_type, code_lines, variables, parameters, vdt) + elif wiener_type == constants.VECTOR_WIENER: + _srk2_pop_var_vector_wiener(sde_type, code_lines, variables, parameters, vdt) + else: + raise ValueError(f'Unknown Wiener type: {wiener_type}, we only ' + f'supports {constants.SUPPORTED_WIENER_TYPE}') + + else: + raise ValueError(f'Unknown var type: {var_type}, we only ' + f'supports {constants.SUPPORTED_VAR_TYPE}') + + # return and compile + common.return_and_compile(code_lines, code_scope, show_code, variables) + return code_scope[func_name] + + +def _srk2_wrapper(): + pass + + +def _wrap(wrapper, f, g, dt, sde_type, var_type, wiener_type, show_code, num_iter): + """The base function to format a SRK method. + + Parameters + ---------- + f : callable + The drift function of the SDE. + g : callable + The diffusion function of the SDE. + dt : float + The numerical precision. + sde_type : str + "utils.ITO_SDE" : Ito's Stochastic Calculus. + "utils.STRA_SDE" : Stratonovich's Stochastic Calculus. + wiener_type : str + var_type : str + "scalar" : with the shape of (). + "population" : with the shape of (N,) or (N1, N2) or (N1, N2, ...). + "system": with the shape of (d, ), (d, N), or (d, N1, N2). + show_code : bool + Whether show the formatted code. + + Returns + ------- + numerical_func : callable + The numerical function. + """ + + sde_type = constants.ITO_SDE if sde_type is None else sde_type + assert sde_type in constants.SUPPORTED_SDE_TYPE, f'Currently, BrainPy only support SDE types: ' \ + f'{constants.SUPPORTED_SDE_TYPE}. But we got {sde_type}.' + + var_type = constants.POPU_VAR if var_type is None else var_type + assert var_type in constants.SUPPORTED_VAR_TYPE, f'Currently, BrainPy only supports variable types: ' \ + f'{constants.SUPPORTED_VAR_TYPE}. But we got {var_type}.' + + wiener_type = constants.SCALAR_WIENER if wiener_type is None else wiener_type + assert wiener_type in constants.SUPPORTED_WIENER_TYPE, f'Currently, BrainPy only supports Wiener ' \ + f'Process types: {constants.SUPPORTED_WIENER_TYPE}. ' \ + f'But we got {wiener_type}.' + + show_code = False if show_code is None else show_code + dt = profile.get_dt() if dt is None else dt + num_iter = 10 if num_iter is None else num_iter + + if f is not None and g is not None: + return wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type, num_iter=num_iter) + + elif f is not None: + return lambda g: wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type, num_iter=num_iter) + + elif g is not None: + return lambda f: wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, + var_type=var_type, wiener_type=wiener_type, num_iter=num_iter) + + else: + raise ValueError('Must provide "f" or "g".') + + +# ------------------ +# Numerical methods +# ------------------ + + +def srk1_strong(f=None, g=None, dt=None, sde_type=None, var_type=None, wiener_type=None, num_iter=None, show_code=None): + return _wrap(_srk1_wrapper, f=f, g=g, dt=dt, sde_type=sde_type, var_type=var_type, + wiener_type=wiener_type, show_code=show_code, num_iter=num_iter) + + +def srk2_strong(f=None, g=None, dt=None, sde_type=None, var_type=None, wiener_type=None, num_iter=None, show_code=None): + return _wrap(_srk2_wrapper, f=f, g=g, dt=dt, sde_type=sde_type, var_type=var_type, + wiener_type=wiener_type, show_code=show_code, num_iter=num_iter) diff --git a/brainpy/integration/utils.py b/brainpy/integrators/sympy_analysis.py similarity index 57% rename from brainpy/integration/utils.py rename to brainpy/integrators/sympy_analysis.py index 796f9455..ff8461ff 100644 --- a/brainpy/integration/utils.py +++ b/brainpy/integrators/sympy_analysis.py @@ -1,10 +1,21 @@ # -*- coding: utf-8 -*- import ast +import inspect import math +from collections import Counter import numpy as np -import sympy + +from brainpy import errors +from brainpy import profile +from brainpy import tools + +try: + import sympy +except ModuleNotFoundError: + raise errors.PackageMissingError('Package "sympy" must be installed when the ' + 'users want to utilize the sympy analysis.') import sympy.functions.elementary.complexes import sympy.functions.elementary.exponential import sympy.functions.elementary.hyperbolic @@ -15,21 +26,16 @@ from sympy.codegen import cfunctions from sympy.printing.precedence import precedence from sympy.printing.str import StrPrinter -from .. import errors -from .. import profile -from .. import tools - -__all__ = [ - 'FUNCTION_MAPPING', - 'CONSTANT_MAPPING', - 'Parser', - 'Printer', - 'str2sympy', - 'sympy2str', - 'get_mapping_scope', - 'DiffEquationAnalyser', - 'analyse_diff_eq', -] + +CONSTANT_NOISE = 'CONSTANT' +FUNCTIONAL_NOISE = 'FUNCTIONAL' + +ODE_TYPE = 'ODE' +SDE_TYPE = 'SDE' + +DIFF_EQUATION = 'diff_equation' +SUB_EXPRESSION = 'sub_expression' + FUNCTION_MAPPING = { # 'real': sympy.functions.elementary.complexes.re, @@ -371,8 +377,9 @@ class Parser(object): class Printer(StrPrinter): """ - Printer that overrides the printing of some basic sympy objects. reversal_potential.g. - print "a and b" instead of "And(a, b)". + Printer that overrides the printing of some basic sympy objects. + + e.g. print "a and b" instead of "And(a, b)". """ def _print_And(self, expr): @@ -425,131 +432,311 @@ def sympy2str(sympy_expr): return _PRINTER.doprint(sympy_expr) -class DiffEquationAnalyser(ast.NodeTransformer): - def __init__(self): - self.variables = [] - self.expressions = [] - self.f_expr = None - self.g_expr = None - self.returns = [] - self.return_type = None +class Expression(object): + def __init__(self, var, code): + self.var_name = var + self.code = code.strip() + self.substituted_code = None - # TODO : Multiple assignment like "a = b = 1" or "a, b = f()" - def visit_Assign(self, node): - targets = node.targets - try: - assert len(targets) == 1 - except AssertionError: - raise errors.DiffEquationError('BrainPy currently does not support multiple ' - 'assignment in differential equation.') - self.variables.append(targets[0].id) - self.expressions.append(tools.ast2code(ast.fix_missing_locations(node.value))) - return node - - def visit_AugAssign(self, node): - var = node.target.id - self.variables.append(var) - op = tools.ast2code(ast.fix_missing_locations(node.op)) - expr = tools.ast2code(ast.fix_missing_locations(node.value)) - self.expressions.append(f"{var} {op} {expr}") - return node - - def visit_AnnAssign(self, node): - raise errors.DiffEquationError('Do not support an assignment with a type annotation.') - - def visit_Return(self, node): - value = node.value - if isinstance(value, (ast.Tuple, ast.List)): # a tuple/list return - v0 = value.elts[0] - if isinstance(v0, (ast.Tuple, ast.List)): # item 0 is a tuple/list - # f expression - if isinstance(v0.elts[0], ast.Name): - self.f_expr = ('_f_res_', v0.elts[0].id) - else: - self.f_expr = ('_f_res_', tools.ast2code(ast.fix_missing_locations(v0.elts[0]))) - - if len(v0.elts) == 1: - self.return_type = '(x,),' - elif len(v0.elts) == 2: - self.return_type = '(x,x),' - # g expression - if isinstance(v0.elts[1], ast.Name): - self.g_expr = ('_g_res_', v0.elts[1].id) - else: - self.g_expr = ('_g_res_', tools.ast2code(ast.fix_missing_locations(v0.elts[1]))) - else: - raise errors.DiffEquationError(f'The dxdt should have the format of (f, g), not ' - f'"({tools.ast2code(ast.fix_missing_locations(v0.elts))})"') + @property + def identifiers(self): + return tools.get_identifiers(self.code) - # returns - for i, item in enumerate(value.elts[1:]): - if isinstance(item, ast.Name): - self.returns.append(item.id) - else: - self.returns.append(tools.ast2code(ast.fix_missing_locations(item))) + def __str__(self): + return f'{self.var_name} = {self.code}' - else: # item 0 is not a tuple/list - # f expression - if isinstance(v0, ast.Name): - self.f_expr = ('_f_res_', v0.id) - else: - self.f_expr = ('_f_res_', tools.ast2code(ast.fix_missing_locations(v0))) - - if len(value.elts) == 1: - self.return_type = 'x,' - elif len(value.elts) == 2: - self.return_type = 'x,x' - # g expression - if isinstance(value.elts[1], ast.Name): - self.g_expr = ('_g_res_', value.elts[1].id) - else: - self.g_expr = ("_g_res_", tools.ast2code(ast.fix_missing_locations(value.elts[1]))) - else: - raise errors.DiffEquationError('Cannot parse return expression. It should have the ' - 'format of "(f, [g]), [*return_values]"') + def __repr__(self): + return self.__str__() + + def __eq__(self, other): + if not isinstance(other, Expression): + return NotImplemented + if self.code != other.code: + return False + if self.var_name != other.var_name: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def get_code(self, subs=True): + if subs: + if self.substituted_code is None: + return self.code + else: + return self.substituted_code else: - self.return_type = 'x' - if isinstance(value, ast.Name): # a name return - self.f_expr = ('_f_res_', value.id) - else: # an expression return - self.f_expr = ('_f_res_', tools.ast2code(ast.fix_missing_locations(value))) - return node + return self.code - def visit_If(self, node): - raise errors.DiffEquationError('Do not support "if" statement in differential equation.') - def visit_IfExp(self, node): - raise errors.DiffEquationError('Do not support "if" expression in differential equation.') +class SingleDiffEq(object): + """Single Differential Equation. - def visit_For(self, node): - raise errors.DiffEquationError('Do not support "for" loop in differential equation.') + A differential equation is defined as the standard form: - def visit_While(self, node): - raise errors.DiffEquationError('Do not support "while" loop in differential equation.') + dx/dt = f(x) + g(x) dW - def visit_Try(self, node): - raise errors.DiffEquationError('Do not support "try" handler in differential equation.') + Parameters + ---------- + func : callable + The user defined differential equation. + """ + + def __init__(self, func): + # check + if func is None: + raise errors.DiffEqError('"func" cannot be None.') + if not (callable(func) and type(func).__name__ == 'function'): + raise errors.DiffEqError('"func" must be a function.') + + # function + self.func = func - def visit_With(self, node): - raise errors.DiffEquationError('Do not support "with" block in differential equation.') + # function string + self.code = tools.deindent(tools.get_main_code(func)) + if 'return' not in self.code: + raise errors.DiffEqError(f'"func" function must return something, ' + f'but found no return.\n{self.code}') + + # function arguments + self.func_args = inspect.getfullargspec(func).args + + # function name + if tools.is_lambda_function(func): + self.func_name = f'_integral_{self.func_args[0]}_' + else: + self.func_name = func.__name__ - def visit_Raise(self, node): - raise errors.DiffEquationError('Do not support "raise" statement in differential equation.') + # function scope + scope = inspect.getclosurevars(func) + self.func_scope = dict(scope.nonlocals) + self.func_scope.update(scope.globals) + + # differential variable name and time name + self.var_name = self.func_args[0] + self.t_name = self.func_args[1] + + # analyse function code + res = analyse_diff_eq(self.code) + self.expressions = [Expression(v, expr) for v, expr in zip(res.variables, res.expressions)] + self.return_type = res.return_type + self.f_expr = None + self.g_expr = None + if res.f_expr is not None: + self.f_expr = Expression(res.f_expr[0], res.f_expr[1]) + if res.g_expr is not None: + self.g_expr = Expression(res.g_expr[0], res.g_expr[1]) + for k, num in Counter(res.variables).items(): + if num > 1: + raise errors.DiffEqError( + f'Found "{k}" {num} times. Please assign each expression ' + f'in differential function with a unique name. ') + + # analyse noise type + self.g_type = CONSTANT_NOISE + self.g_value = None + if self.g_expr is not None: + self._substitute(self.g_expr, self.expressions) + g_code = self.g_expr.get_code(subs=True) + for idf in tools.get_identifiers(g_code): + if idf not in self.func_scope: + self.g_type = FUNCTIONAL_NOISE + break + else: + self.g_value = eval(g_code, self.func_scope) - def visit_Delete(self, node): - raise errors.DiffEquationError('Do not support "del" operation in differential equation.') + def _substitute(self, final_exp, expressions, substitute_vars=None): + """Substitute expressions to get the final single expression + Parameters + ---------- + final_exp : Expression + The final expression. + expressions : list, tuple + The list/tuple of expressions. + """ + if substitute_vars is None: + return + if final_exp is None: + return + assert substitute_vars == 'all' or \ + substitute_vars == self.var_name or \ + isinstance(substitute_vars, (tuple, list)) + + # Goal: Substitute dependent variables into the expresion + # Hint: This step doesn't require the left variables are unique + dependencies = {} + for expr in expressions: + substitutions = {} + for dep_var, dep_expr in dependencies.items(): + if dep_var in expr.identifiers: + code = dep_expr.get_code(subs=True) + substitutions[sympy.Symbol(dep_var, real=True)] = str2sympy(code).expr + if len(substitutions): + new_sympy_expr = str2sympy(expr.code).expr.xreplace(substitutions) + new_str_expr = sympy2str(new_sympy_expr) + expr.substituted_code = new_str_expr + dependencies[expr.var_name] = expr + else: + if substitute_vars == 'all': + dependencies[expr.var_name] = expr + elif substitute_vars == self.var_name: + if self.var_name in expr.identifiers: + dependencies[expr.var_name] = expr + else: + ids = expr.identifiers + for var in substitute_vars: + if var in ids: + dependencies[expr.var_name] = expr + break + + # Goal: get the final differential equation + # Hint: the step requires the expression variables must be unique + substitutions = {} + for dep_var, dep_expr in dependencies.items(): + code = dep_expr.get_code(subs=True) + substitutions[sympy.Symbol(dep_var, real=True)] = str2sympy(code).expr + if len(substitutions): + new_sympy_expr = str2sympy(final_exp.code).expr.xreplace(substitutions) + new_str_expr = sympy2str(new_sympy_expr) + final_exp.substituted_code = new_str_expr + + def get_f_expressions(self, substitute_vars=None): + if self.f_expr is None: + return [] + self._substitute(self.f_expr, self.expressions, substitute_vars=substitute_vars) + + return_expressions = [] + # the derivative expression + dif_eq_code = self.f_expr.get_code(subs=True) + return_expressions.append(Expression(f'_df{self.var_name}_dt', dif_eq_code)) + # needed variables + need_vars = tools.get_identifiers(dif_eq_code) + need_vars |= tools.get_identifiers(', '.join(self.return_intermediates)) + # get the total return expressions + for expr in self.expressions[::-1]: + if expr.var_name in need_vars: + if expr.substituted_code is None: + code = expr.code + else: + code = expr.substituted_code + return_expressions.append(Expression(expr.var_name, code)) + need_vars |= tools.get_identifiers(code) + return return_expressions[::-1] + + def get_g_expressions(self): + if self.g_expr is None: + return [] + + if self.is_functional_noise: + return_expressions = [] + # the derivative expression + eq_code = self.g_expr.get_code(subs=True) + return_expressions.append(Expression(f'_dg{self.var_name}_dt', eq_code)) + # needed variables + need_vars = tools.get_identifiers(eq_code) + # get the total return expressions + for expr in self.expressions[::-1]: + if expr.var_name in need_vars: + if expr.substituted_code is None: + code = expr.code + else: + code = expr.substituted_code + return_expressions.append(Expression(expr.var_name, code)) + need_vars |= tools.get_identifiers(code) + return return_expressions[::-1] + else: + return [Expression(f'_dg{self.var_name}_dt', self.g_expr.get_code(subs=True))] + + def _replace_expressions(self, expressions, name, y_sub, t_sub=None): + """Replace expressions of df part. + + Parameters + ---------- + expressions : list, tuple + The list/tuple of expressions. + name : str + The name of the new expression. + y_sub : str + The new name of the variable "y". + t_sub : str, optional + The new name of the variable "t". + + Returns + ------- + list_of_expr : list + A list of expressions. + """ + return_expressions = [] + + # replacements + replacement = {self.var_name: y_sub} + if t_sub is not None: + replacement[self.t_name] = t_sub + + # replace variables in expressions + for expr in expressions: + replace = False + identifiers = expr.identifiers + for repl_var in replacement.keys(): + if repl_var in identifiers: + replace = True + break + if replace: + code = tools.word_replace(expr.code, replacement) + new_expr = Expression(f"{expr.var_name}_{name}", code) + return_expressions.append(new_expr) + replacement[expr.var_name] = new_expr.var_name + return return_expressions + + def replace_f_expressions(self, name, y_sub, t_sub=None): + """Replace expressions of df part. + + Parameters + ---------- + name : str + The name of the new expression. + y_sub : str + The new name of the variable "y". + t_sub : str, optional + The new name of the variable "t". + + Returns + ------- + list_of_expr : list + A list of expressions. + """ + return self._replace_expressions(self.get_f_expressions(), + name=name, + y_sub=y_sub, + t_sub=t_sub) + + def replace_g_expressions(self, name, y_sub, t_sub=None): + if self.is_functional_noise: + return self._replace_expressions(self.get_g_expressions(), + name=name, + y_sub=y_sub, + t_sub=t_sub) + else: + return [] + + @property + def is_stochastic(self): + if self.g_expr is not None: + try: + if eval(self.g_expr.code, self.func_scope) == 0.: + return False + except Exception as e: + pass + return True + else: + return False -def analyse_diff_eq(eq_code): - assert eq_code.strip() != '' - tree = ast.parse(eq_code) - analyser = DiffEquationAnalyser() - analyser.visit(tree) + @property + def is_functional_noise(self): + return self.g_type == FUNCTIONAL_NOISE - res = tools.DictPlus(variables=analyser.variables, - expressions=analyser.expressions, - return_intermediates=analyser.returns, - return_type=analyser.return_type, - f_expr=analyser.f_expr, - g_expr=analyser.g_expr) - return res + @property + def expr_names(self): + return [expr.var_name for expr in self.expressions] diff --git a/brainpy/integrators/utils.py b/brainpy/integrators/utils.py new file mode 100644 index 00000000..0f3a1b82 --- /dev/null +++ b/brainpy/integrators/utils.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +import inspect + +from brainpy import errors +from brainpy import profile + +__all__ = [ + 'get_args', +] + + +def get_args(f): + """Get the function arguments. + + >>> def f1(a, b, t, *args, c=1): pass + >>> get_args(f1) + (['a', 'b'], ['t', '*args', 'c'], ['a', 'b', 't', '*args', 'c=1']) + + >>> def f2(a, b, *args, c=1, **kwargs): pass + >>> get_args(f2) + ValueError: Don not support dict of keyword arguments: **kwargs + + >>> def f3(a, b, t, c=1, d=2): pass + >>> get_args(f4) + (['a', 'b'], ['t', 'c', 'd'], ['a', 'b', 't', 'c=1', 'd=2']) + + >>> def f4(a, b, t, *args): pass + >>> get_args(f4) + (['a', 'b'], ['t', '*args'], ['a', 'b', 't', '*args']) + + >>> scope = {} + >>> exec(compile('def f5(a, b, t, *args): pass', '', 'exec'), scope) + >>> get_args(scope['f5']) + (['a', 'b'], ['t', '*args'], ['a', 'b', 't', '*args']) + + Parameters + ---------- + f : callable + The function. + + Returns + ------- + args : tuple + The variable names, the other arguments, and the original args. + """ + + # 1. get the function arguments + reduced_args = [] + original_args = [] + + for name, par in inspect.signature(f).parameters.items(): + if par.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: + reduced_args.append(par.name) + + elif par.kind is inspect.Parameter.VAR_POSITIONAL: + reduced_args.append(f'*{par.name}') + + elif par.kind is inspect.Parameter.KEYWORD_ONLY: + reduced_args.append(par.name) + + elif par.kind is inspect.Parameter.POSITIONAL_ONLY: + raise errors.DiffEqError('Don not support positional only parameters, e.g., /') + elif par.kind is inspect.Parameter.VAR_KEYWORD: + raise errors.DiffEqError(f'Don not support dict of keyword arguments: {str(par)}') + else: + raise errors.DiffEqError(f'Unknown argument type: {par.kind}') + + original_args.append(str(par)) + + # 2. analyze the function arguments + # 2.1 class keywords + class_kw = [] + if reduced_args[0] in profile.CLASS_KEYWORDS: + class_kw.append(reduced_args[0]) + reduced_args = reduced_args[1:] + for a in reduced_args: + if a in profile.CLASS_KEYWORDS: + raise errors.DiffEqError(f'Class keywords "{a}" must be defined ' + f'as the first argument.') + # 2.2 variable names + var_names = [] + for a in reduced_args: + if a == 't': + break + var_names.append(a) + else: + raise ValueError('Do not find time variable "t".') + other_args = reduced_args[len(var_names):] + return class_kw, var_names, other_args, original_args diff --git a/brainpy/measure.py b/brainpy/measure.py index e297f0cd..a3b4c30c 100644 --- a/brainpy/measure.py +++ b/brainpy/measure.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- import numpy as np -from numba import njit -from . import profile +from brainpy import profile + +try: + from numba import njit +except ModuleNotFoundError: + njit = None __all__ = [ 'cross_correlation', @@ -18,13 +22,16 @@ __all__ = [ ############################### -@njit def _cc(states, i, j): sqrt_ij = np.sqrt(np.sum(states[i]) * np.sum(states[j])) k = 0. if sqrt_ij == 0. else np.sum(states[i] * states[j]) / sqrt_ij return k +if njit is None: + _cc = njit(_cc) + + def cross_correlation(spikes, bin_size): """Calculate cross correlation index between neurons. diff --git a/brainpy/profile.py b/brainpy/profile.py index 83f2eb42..27ad55bb 100644 --- a/brainpy/profile.py +++ b/brainpy/profile.py @@ -4,299 +4,24 @@ The setting of the overall framework by ``profile.py`` API. """ -from numba import cuda - __all__ = [ - 'set', - - 'run_on_cpu', - 'run_on_gpu', - - 'set_backend', - 'get_backend', - - 'set_device', - 'get_device', + 'set_class_keywords', 'set_dt', 'get_dt', 'set_numerical_method', 'get_numerical_method', - - 'set_numba_profile', - 'get_numba_profile', - - 'get_num_thread_gpu', - - 'is_jit', - 'is_merge_integrators', - 'is_merge_steps', - 'is_substitute_equation', - 'show_code_scope', - 'show_format_code', ] -_jit = False -_backend = 'numpy' -_device = 'cpu' _dt = 0.1 _method = 'euler' -_numba_setting = { - 'nopython': True, - 'fastmath': True, - 'nogil': True, - 'parallel': False -} -_show_format_code = False -_show_code_scope = False -_substitute_equation = False -_merge_integrators = True -_merge_steps = False -_num_thread_gpu = None - - -def set( - jit=None, - device=None, - numerical_method=None, - dt=None, - float_type=None, - int_type=None, - merge_integrators=None, - merge_steps=None, - substitute=None, - show_code=None, - show_code_scope=None -): - # JIT and device - if device is not None and jit is None: - assert isinstance(device, str), "'device' must a string." - set_device(_jit, device=device) - if jit is not None: - assert isinstance(jit, bool), "'jit' must be True or False." - if device is not None: - assert isinstance(device, str), "'device' must a string." - set_device(jit, device=device) - - # numerical integration method - if numerical_method is not None: - assert isinstance(numerical_method, str), '"numerical_method" must be a string.' - set_numerical_method(numerical_method) - - # numerical integration precision - if dt is not None: - assert isinstance(dt, (float, int)), '"dt" must be float or int.' - set_dt(dt) - - # default float type - if float_type is not None: - from .backend import _set_default_float - _set_default_float(float_type) - - # default int type - if int_type is not None: - from .backend import _set_default_int - _set_default_int(int_type) - - # option to merge integral functions - if merge_integrators is not None: - assert isinstance(merge_integrators, bool), '"merge_integrators" must be True or False.' - if run_on_gpu() and not merge_integrators: - raise ValueError('GPU mode do not support "merge_integrators = False".') - global _merge_integrators - _merge_integrators = merge_integrators - - # option to merge step functions - if merge_steps is not None: - assert isinstance(merge_steps, bool), '"merge_steps" must be True or False.' - global _merge_steps - _merge_steps = merge_steps - - # option of the equation substitution - if substitute is not None: - assert isinstance(substitute, bool), '"substitute" must be True or False.' - global _substitute_equation - _substitute_equation = substitute - - # option of the formatted code output - if show_code is not None: - assert isinstance(show_code, bool), '"show_code" must be True or False.' - global _show_format_code - _show_format_code = show_code - - # option of the formatted code scope - if show_code_scope is not None: - assert isinstance(show_code_scope, bool), '"show_code_scope" must be True or False.' - global _show_code_scope - _show_code_scope = show_code_scope - - -def set_device(jit, device=None): - """Set the backend and the device to deploy the models. - - Parameters - ---------- - jit : bool - Whether use the jit acceleration. - device : str, optional - The device name. - """ +CLASS_KEYWORDS = ['self', 'cls'] - # jit - # --- - global _jit - - if _jit != jit: - _jit = jit - - # device - # ------ - - global _device - global _num_thread_gpu - - if device is None: - return - - device = device.lower() - if _device != device: - if not jit: - if device != 'cpu': - print(f'Non-JIT mode now only supports "cpu" device, not "{device}".') - else: - _device = device - else: - if device == 'cpu': - set_numba_profile(parallel=False) - elif device == 'multi-cpu': - set_numba_profile(parallel=True) - else: - if device.startswith('gpu'): - # get cuda id - cuda_id = device.replace('gpu', '') - if cuda_id == '': - cuda_id = 0 - device = f'{device}0' - else: - cuda_id = float(cuda_id) - - # set cuda - if cuda.is_available(): - cuda.select_device(cuda_id) - else: - raise ValueError('Cuda is not available. Cannot set gpu backend.') - - gpu = cuda.get_current_device() - _num_thread_gpu = gpu.MAX_THREADS_PER_BLOCK - - else: - raise ValueError(f'Unknown device in Numba mode: {device}.') - _device = device - - -def get_device(): - """Get the device name. - - Returns - ------- - device: str - Device name. - - """ - return _device - - -def is_jit(): - """Check whether the backend is ``numba``. - - Returns - ------- - jit : bool - True or False. - """ - return _jit - - -def run_on_cpu(): - """Check whether the device is "CPU". - - Returns - ------- - device : bool - True or False. - """ - return _device.endswith('cpu') - - -def run_on_gpu(): - """Check whether the device is "GPU". - - Returns - ------- - device : bool - True or False. - """ - return _device.startswith('gpu') - - -def set_backend(backend): - """Set the running backend. - - Parameters - ---------- - backend : str - The backend name. - """ - if backend not in ['numpy', 'pytorch']: - raise ValueError(f'BrainPy now supports "numpy" or "pytorch" backend, not "{backend}".') - - global _backend - _backend = backend - - -def get_backend(): - """Get the used backend of BrainPy. - - Returns - ------- - backend : str - The backend name. - """ - return _backend - - -def set_numba_profile(**kwargs): - """Set the compilation options of Numba JIT function. - - Parameters - ---------- - kwargs : Any - The arguments, including ``cache``, ``fastmath``, - ``parallel``, ``nopython``. - """ - global _numba_setting - - if 'fastmath' in kwargs: - _numba_setting['fastmath'] = kwargs.pop('fastmath') - if 'nopython' in kwargs: - _numba_setting['nopython'] = kwargs.pop('nopython') - if 'nogil' in kwargs: - _numba_setting['nogil'] = kwargs.pop('nogil') - if 'parallel' in kwargs: - _numba_setting['parallel'] = kwargs.pop('parallel') - - -def get_numba_profile(): - """Get the compilation setting of numba JIT function. - - Returns - ------- - numba_setting : dict - Numba setting. - """ - return _numba_setting +def set_class_keywords(*args): + global CLASS_KEYWORDS + CLASS_KEYWORDS = list(args) def set_dt(dt): @@ -331,7 +56,7 @@ def set_numerical_method(method): method : str, callable Numerical integrator method. """ - from brainpy.integration import _SUPPORT_METHODS + from brainpy.integrators import _SUPPORT_METHODS if not isinstance(method, str): raise ValueError(f'Only support string, not {type(method)}.') @@ -351,27 +76,3 @@ def get_numerical_method(): The default numerical integrator method. """ return _method - - -def is_merge_integrators(): - return _merge_integrators - - -def is_merge_steps(): - return _merge_steps - - -def is_substitute_equation(): - return _substitute_equation - - -def show_code_scope(): - return _show_code_scope - - -def show_format_code(): - return _show_format_code - - -def get_num_thread_gpu(): - return _num_thread_gpu diff --git a/brainpy/simulation/__init__.py b/brainpy/simulation/__init__.py new file mode 100644 index 00000000..b4aa13e2 --- /dev/null +++ b/brainpy/simulation/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from .population import * +from .network import * diff --git a/brainpy/simulation/constants.py b/brainpy/simulation/constants.py new file mode 100644 index 00000000..60af9453 --- /dev/null +++ b/brainpy/simulation/constants.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + + +NEU_GROUP_TYPE = 'NeuGroup' # name of the neuron group +SYN_CONN_TYPE = 'SynConn' # name of the synapse connection +TWO_END_TYPE = 'TwoEndConn' # name of the two-end synaptic connection +SUPPORTED_TYPES = [NEU_GROUP_TYPE, SYN_CONN_TYPE, TWO_END_TYPE] + +# input operations +SUPPORTED_INPUT_OPS = {'-': 'sub', + '+': 'add', + 'x': 'mul', + '*': 'mul', + '/': 'div', + '=': 'assign'} diff --git a/brainpy/simulation/monitors.py b/brainpy/simulation/monitors.py new file mode 100644 index 00000000..31cd0b32 --- /dev/null +++ b/brainpy/simulation/monitors.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +from brainpy import backend +from brainpy import errors +from brainpy import tools + +__all__ = [ + 'Monitor' +] + + +class Monitor(tools.DictPlus): + """The basic Monitor class to store the past variable trajectories. + """ + def __init__(self, variables): + mon_items = [] + mon_indices = [] + item_content = {} + if variables is not None: + if isinstance(variables, (list, tuple)): + for var in variables: + if isinstance(var, str): + mon_items.append(var) + mon_indices.append(None) + item_content[var] = backend.zeros((1, 1)) + elif isinstance(var, (tuple, list)): + mon_items.append(var[0]) + mon_indices.append(var[1]) + item_content[var[0]] = backend.zeros((1, 1)) + else: + raise errors.ModelUseError(f'Unknown monitor item: {str(var)}') + elif isinstance(variables, dict): + for k, v in variables.items(): + mon_items.append(k) + mon_indices.append(v) + item_content[k] = backend.zeros((1, 1)) + else: + raise errors.ModelUseError(f'Unknown monitors type: {type(variables)}') + super(Monitor, self).__init__(ts=None, + vars=mon_items, + indices=mon_indices, + num_item=len(item_content), + **item_content) + + def reshape(self, run_length): + for var in self['vars']: + val = self[var] + shape = backend.shape(val) + if run_length < shape[0]: + self[var] = val[:run_length] + elif run_length > shape[0]: + append = backend.zeros((run_length - shape[0],) + shape[1:]) + self[var] = backend.vstack([val, append]) diff --git a/brainpy/simulation/network.py b/brainpy/simulation/network.py new file mode 100644 index 00000000..b8260d86 --- /dev/null +++ b/brainpy/simulation/network.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +from collections import OrderedDict + +from brainpy import backend +from brainpy import profile +from brainpy.simulation import population +from brainpy.simulation import utils + +__all__ = [ + 'Network', +] + + +class Network(object): + """The main simulation controller in ``BrainPy``. + + ``Network`` handles the running of a simulation. It contains a set + of objects that are added with `add()`. The `run()` method actually + runs the simulation. The main loop runs according to user add orders. + The objects in the `Network` are accessible via their names, e.g. + `net.name` would return the `object`. + """ + + def __init__(self, *args, show_code=False, **kwargs): + # record the current step + self.t_start = 0. + self.t_end = 0. + + # store all nodes + self.all_nodes = OrderedDict() + + # store the step function + self.run_func = None + self.show_code = show_code + + # add nodes + self.add(*args, **kwargs) + + def __getattr__(self, item): + if item in self.all_nodes: + return self.all_nodes[item] + else: + return super(Network, self).__getattribute__(item) + + def _add_obj(self, obj, name=None): + # 1. check object type + if not isinstance(obj, population.Population): + raise ValueError(f'Unknown object type "{type(obj)}". ' + f'Currently, Network only supports ' + f'{population.NeuGroup.__name__} and ' + f'{population.TwoEndConn.__name__}.') + # 2. check object name + name = obj.name if name is None else name + if name in self.all_nodes: + raise KeyError(f'Name "{name}" has been used in the network, ' + f'please change another name.') + # 3. add object to the network + self.all_nodes[name] = obj + if obj.name != name: + self.all_nodes[obj.name] = obj + + def add(self, *args, **kwargs): + """Add object (neurons or synapses) to the network. + + Parameters + ---------- + args + The nameless objects. + kwargs + The named objects, which can be accessed by `net.xxx` + (xxx is the name of the object). + """ + for obj in args: + self._add_obj(obj) + for name, obj in kwargs.items(): + self._add_obj(obj, name) + + def run(self, duration, inputs=(), report=False, report_percent=0.1): + """Run the simulation for the given duration. + + This function provides the most convenient way to run the network. + For example: + + Parameters + ---------- + duration : int, float, tuple, list + The amount of simulation time to run for. + inputs : list, tuple + The receivers, external inputs and durations. + report : bool + Report the progress of the simulation. + report_percent : float + The speed to report simulation progress. + """ + # preparation + start, end = utils.check_duration(duration) + dt = profile.get_dt() + ts = backend.arange(start, end, dt) + + # build the network + run_length = ts.shape[0] + format_inputs = utils.format_net_level_inputs(inputs, run_length) + net_runner = backend.get_net_runner()(all_nodes=self.all_nodes) + self.run_func = net_runner.build(run_length=run_length, + formatted_inputs=format_inputs, + return_code=False, + show_code=self.show_code) + + # run the network + utils.run_model(self.run_func, times=ts, report=report, report_percent=report_percent) + + # end + self.t_start, self.t_end = start, end + + @property + def ts(self): + """Get the time points of the network. + """ + return backend.arange(self.t_start, self.t_end, profile.get_dt()) diff --git a/brainpy/simulation/population.py b/brainpy/simulation/population.py new file mode 100644 index 00000000..0e6529e9 --- /dev/null +++ b/brainpy/simulation/population.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- + +from brainpy import backend +from brainpy import connectivity +from brainpy import errors +from brainpy import profile +from brainpy.simulation import constants +from brainpy.simulation import utils +from brainpy.simulation.monitors import Monitor + +__all__ = [ + 'Population', + 'NeuGroup', + 'TwoEndConn', +] + +_NeuGroup_NO = 0 +_TwoEndSyn_NO = 0 + + +class Population(object): + """Base Population Class. + + Parameters + ---------- + name : str + The name of the (neurons/synapses) ensemble. + size : int + The number of the neurons/synapses. + steps : function, list of function + The callable function, or a list of callable functions. + monitors : list, tuple, None + Variables to monitor. + ensemble_type : str + Class type. + """ + + target_backend = None + + def __init__(self, size, steps, monitors, ensemble_type, name, + host=None, show_code=False): + # host of the data + # ---------------- + if host is None: + host = self + self.host = host + + # ensemble type + # ------------- + if ensemble_type not in constants.SUPPORTED_TYPES: + print(f'Ensemble type {ensemble_type} is not registered in BrainPy. Currently, ' + f'BrainPy has recognized "{constants.SUPPORTED_TYPES}".') + self.ensemble_type = ensemble_type + + # model + # ----- + if callable(steps): + self.steps = [steps] + elif isinstance(steps, (list, tuple)) and callable(steps[0]): + self.steps = list(steps) + else: + raise errors.ModelDefError(f'Unknown model type: {type(steps)}. Currently, BrainPy ' + f'only supports: function, list of functions.') + + # size + # ---- + if isinstance(size, (list, tuple)): + if len(size) <= 0: + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + if not isinstance(size[0], int): + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + size = tuple(size) + elif isinstance(size, int): + size = (size,) + else: + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + self.size = size + + # name + # ---- + if not name.isidentifier(): + raise errors.ModelUseError( + f'"{name}" isn\'t a valid identifier according to Python ' + f'language definition. Please choose another name.') + self.name = name + + # monitors + # --------- + self.mon = Monitor(monitors) + for var in self.mon['vars']: + if not hasattr(self, var): + raise errors.ModelDefError(f"Item {var} isn't defined in model {self}, " + f"so it can not be monitored.") + + # runner + # ------- + self.runner = backend.get_node_runner()(pop=self) + + # run function + # ------------ + self.run_func = None + + # others + # --- + self.show_code = show_code + if self.target_backend is None: + raise errors.ModelDefError('Must define "target_backend".') + if isinstance(self.target_backend, str): + self.target_backend = [self.target_backend] + assert isinstance(self.target_backend, (tuple, list)), 'target_backend must be a list/tuple.' + + def build(self, format_inputs, return_code=True, mon_length=0): + """Build the object for running. + + Parameters + ---------- + format_inputs : list, tuple, optional + The object inputs. + return_code : bool + Whether return the formatted codes. + mon_length : int + The monitor length. + + Returns + ------- + calls : list, tuple + The code lines to call step functions. + """ + if backend.get_backend() not in self.target_backend: + raise errors.ModelDefError(f'The model {self.name} is target to run on {self.target_backend},' + f'but currently the default backend of BrainPy is ' + f'{profile.get_backend()}') + return self.runner.build(formatted_inputs=format_inputs, + mon_length=mon_length, + return_code=return_code, + show_code=self.show_code) + + def run(self, duration, inputs=(), report=False, report_percent=0.1): + """The running function. + + Parameters + ---------- + duration : float, int, tuple, list + The running duration. + inputs : list, tuple + The model inputs with the format of ``[(key, value [operation])]``. + report : bool + Whether report the running progress. + report_percent : float + The percent of progress to report. + """ + + # times + # ------ + start, end = utils.check_duration(duration) + times = backend.arange(start, end, profile.get_dt()) + run_length = backend.shape(times)[0] + + # build run function + # ------------------ + format_inputs = utils.format_pop_level_inputs(inputs, self, run_length, self.size) + self.run_func = self.build(format_inputs, mon_length=run_length, return_code=False) + + # run the model + # ------------- + utils.run_model(self.run_func, times, report, report_percent) + self.mon['ts'] = times + + def get_schedule(self): + """Get the schedule (running order) of the update functions. + + Returns + ------- + schedule : list, tuple + The running order of update functions. + """ + return self.runner.get_schedule() + + def set_schedule(self, schedule): + """Set the schedule (running order) of the update functions. + + For example, if the ``self.model`` has two step functions: `step1`, `step2`. + Then, you can set the shedule by using: + + >>> pop = Population(...) + >>> pop.set_schedule(['input', 'step1', 'step2', 'monitor']) + """ + self.runner.set_schedule(schedule) + + def __str__(self): + return self.name + + +class NeuGroup(Population): + """Neuron Group. + + Parameters + ---------- + steps : NeuType + The instantiated neuron type model. + size : int, tuple + The neuron group geometry. + monitors : list, tuple + Variables to monitor. + name : str + The name of the neuron group. + """ + + def __init__(self, size, steps, monitors=None, name=None, + host=None, show_code=False): + # name + # ----- + if name is None: + name = 'NeuGroup' + global _NeuGroup_NO + _NeuGroup_NO += 1 + name = f'NG{_NeuGroup_NO}_{name}' + + # initialize + # ---------- + super(NeuGroup, self).__init__(size=size, + steps=steps, + monitors=monitors, + name=name, + host=host, + ensemble_type=constants.NEU_GROUP_TYPE, + show_code=show_code) + + +class TwoEndConn(Population): + """Two End Synaptic Connections. + + Parameters + ---------- + steps : SynType + The instantiated neuron type model. + pre : neurons.NeuGroup, neurons.NeuSubGroup + Pre-synaptic neuron group. + post : neurons.NeuGroup, neurons.NeuSubGroup + Post-synaptic neuron group. + conn : connectivity.Connector + Connection method to create synaptic connectivity. + monitors : list, tuple + Variables to monitor. + name : str + The name of the neuron group. + """ + + def __init__(self, steps, pre=None, post=None, conn=None, monitors=None, + name=None, host=None, show_code=False): + # name + # ---- + if name is None: + name = 'TwoEndConn' + global _TwoEndSyn_NO + _TwoEndSyn_NO += 1 + name = f'TEC{_TwoEndSyn_NO}_{name}' + + # pre or post neuron group + # ------------------------ + self.pre = pre + self.post = post + self.conn = None + if pre is not None and post is not None: + if not isinstance(pre, NeuGroup): + raise errors.ModelUseError('"pre" must be an instance of NeuGroup.') + if not isinstance(post, NeuGroup): + raise errors.ModelUseError('"post" must be an instance of NeuGroup.') + + if conn is not None: + if isinstance(conn, connectivity.Connector): + self.conn = conn(pre.size, post.size) + self.conn = connectivity.Connector() + + size = 1 # TODO + + # initialize + # ---------- + super(TwoEndConn, self).__init__(steps=steps, + name=name, + size=size, + monitors=monitors, + ensemble_type=constants.SYN_CONN_TYPE, + host=host, + show_code=show_code) diff --git a/brainpy/simulation/runner.py b/brainpy/simulation/runner.py new file mode 100644 index 00000000..ec37eecc --- /dev/null +++ b/brainpy/simulation/runner.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +import abc +from brainpy import errors + + +__all__ = [ + 'AbstractRunner', + 'NodeRunner', + 'NetRunner', +] + + +class AbstractRunner(abc.ABC): + """ + Abstract base class for backend runner. + """ + @abc.abstractmethod + def build(self, *args, **kwargs): + pass + + +class NodeRunner(AbstractRunner): + """ + Abstract Node Runner. + """ + def __init__(self, host, steps): + self.host = host + assert isinstance(steps, (list, tuple)) and callable(steps[0]) + self.steps = steps + self.step_names = [step.__name__ for step in steps] + self.schedule = ['input'] + self.step_names + ['monitor'] + + def get_schedule(self): + return self.schedule + + def set_schedule(self, schedule): + if not isinstance(schedule, (list, tuple)): + raise errors.ModelUseError('"schedule" must be a list/tuple.') + all_func_names = ['input', 'monitor'] + self.step_names + for s in schedule: + if s not in all_func_names: + raise errors.ModelUseError(f'Unknown step function "{s}" for model "{self.state}".') + self.schedule = schedule + + @abc.abstractmethod + def set_data(self, *args, **kwargs): + pass + + @abc.abstractmethod + def get_input_func(self, *args, **kwargs): + pass + + @abc.abstractmethod + def get_monitor_func(self, *args, **kwargs): + pass + + @abc.abstractmethod + def get_steps_func(self, *args, **kwargs): + pass + + +class NetRunner(AbstractRunner): + """ + Abstract Network Runner. + """ + def __init__(self, all_nodes): + self.all_nodes = all_nodes diff --git a/brainpy/simulation/utils.py b/brainpy/simulation/utils.py new file mode 100644 index 00000000..1f88c4d8 --- /dev/null +++ b/brainpy/simulation/utils.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- + +import time + +from brainpy import backend +from brainpy import errors +from brainpy import profile +from brainpy.simulation import constants + + +__all__ = [ + 'check_duration', + 'run_model', + 'format_pop_level_inputs', + 'format_net_level_inputs', +] + + +def check_duration(duration): + """Check the running duration. + + Parameters + ---------- + duration : int, list, tuple + The running duration, it can be an int (which represents the end + of the simulation), of a tuple/list of int (which represents the + [start, end] / [end, start] of the simulation). + + Returns + ------- + duration : tuple + The tuple of running duration includes (start, end). + """ + if isinstance(duration, (int, float)): + start, end = 0., duration + elif isinstance(duration, (tuple, list)): + assert len(duration) == 2, 'Only support duration setting with the ' \ + 'format of "(start, end)" or "end".' + start, end = duration + else: + raise ValueError(f'Unknown duration type: {type(duration)}. Currently, BrainPy only ' + f'support duration specification with the format of "(start, end)" ' + f'or "end".') + + if start > end: + start, end = end, start + return start, end + + +def run_model(run_func, times, report, report_percent): + """Run the model. + + The "run_func" can be the step run function of a population, or a network. + + Parameters + ---------- + run_func : callable + The step run function. + times : iterable + The model running times. + report : bool + Whether report the progress of the running. + report_percent : float + The percent of the total running length for each report. + """ + run_length = len(times) + dt = profile.get_dt() + if report: + t0 = time.time() + for i, t in enumerate(times[:2]): + run_func(_t=t, _i=i, _dt=dt) + print('Compilation used {:.4f} s.'.format(time.time() - t0)) + + print("Start running ...") + report_gap = int(run_length * report_percent) + t0 = time.time() + for run_idx in range(2, run_length): + run_func(_t=times[run_idx], _i=run_idx, _dt=dt) + if (run_idx + 1) % report_gap == 0: + percent = (run_idx + 1) / run_length * 100 + print('Run {:.1f}% used {:.3f} s.'.format(percent, time.time() - t0)) + print('Simulation is done in {:.3f} s.'.format(time.time() - t0)) + print() + else: + for run_idx in range(run_length): + run_func(_t=times[run_idx], _i=run_idx, _dt=dt) + + +def format_pop_level_inputs(inputs, host, mon_length, size): + """Format the inputs of a population. + + Parameters + ---------- + inputs : tuple, list + The inputs of the population. + host : Population + The host which contains all data. + mon_length : int + The monitor length. + size : tuple + The size of the population. + + Returns + ------- + formatted_inputs : tuple, list + The formatted inputs of the population. + """ + if inputs is None: + inputs = [] + if not isinstance(inputs, (tuple, list)): + raise errors.ModelUseError('"inputs" must be a tuple/list.') + if len(inputs) > 0 and not isinstance(inputs[0], (list, tuple)): + if isinstance(inputs[0], str): + inputs = [inputs] + else: + raise errors.ModelUseError('Unknown input structure, only support inputs ' + 'with format of "(key, value, [operation])".') + for input in inputs: + if not 2 <= len(input) <= 3: + raise errors.ModelUseError('For each target, you must specify "(key, value, [operation])".') + if len(input) == 3 and input[2] not in constants.SUPPORTED_INPUT_OPS: + raise errors.ModelUseError(f'Input operation only supports ' + f'"{list(constants.SUPPORTED_INPUT_OPS.keys())}", ' + f'not "{input[2]}".') + + # format inputs + # ------------- + formatted_inputs = [] + for input in inputs: + # key + if not isinstance(input[0], str): + raise errors.ModelUseError('For each input, input[0] must be a string ' + 'to specify variable of the target.') + key = input[0] + if not hasattr(host, key): + raise errors.ModelUseError(f'Input target key "{key}" is not defined in {host}.') + + # value and data type + val = input[1] + if isinstance(input[1], (int, float)): + data_type = 'fix' + else: + shape = backend.shape(input[1]) + if shape[0] == mon_length: + data_type = 'iter' + elif shape == size: + data_type = 'fix' + else: + raise errors.ModelUseError(f'Unknown size of input for "{key}", ' + f'it should either be {size}, nor be ' + f'the shape of {(mon_length, ) + size}') + + # operation + if len(input) == 3: + ops = input[2] + else: + ops = '+' + if ops not in constants.SUPPORTED_INPUT_OPS: + raise errors.ModelUseError(f'Currently, BrainPy only support operations ' + f'{list(constants.SUPPORTED_INPUT_OPS.keys())}, ' + f'not {ops}') + # input + format_inp = (key, val, ops, data_type) + formatted_inputs.append(format_inp) + + return formatted_inputs + + +def format_net_level_inputs(inputs, run_length): + """Format the inputs of a network. + + Parameters + ---------- + inputs : tuple + The inputs. + run_length : int + The running length. + + Returns + ------- + formatted_input : dict + The formatted input. + """ + from brainpy.simulation import population + + # 1. format the inputs to standard + # formats and check the inputs + if not isinstance(inputs, (tuple, list)): + raise errors.ModelUseError('"inputs" must be a tuple/list.') + if len(inputs) > 0 and not isinstance(inputs[0], (list, tuple)): + if isinstance(inputs[0], population.Population): + inputs = [inputs] + else: + raise errors.ModelUseError('Unknown input structure. Only supports ' + '"(target, key, value, [operation])".') + for input in inputs: + if not 3 <= len(input) <= 4: + raise errors.ModelUseError('For each target, you must specify ' + '"(target, key, value, [operation])".') + if len(input) == 4: + if input[3] not in constants.SUPPORTED_INPUT_OPS: + raise errors.ModelUseError(f'Input operation only supports ' + f'"{list(constants.SUPPORTED_INPUT_OPS.keys())}", ' + f'not "{input[3]}".') + + # 2. format inputs + formatted_inputs = {} + for input in inputs: + # target + if isinstance(input[0], population.Population): + target = input[0] + target_name = input[0].name + else: + raise KeyError(f'Unknown input target: {str(input[0])}') + + # key + key = input[1] + if not isinstance(key, str): + raise errors.ModelUseError('For each input, input[1] must be a string ' + 'to specify variable of the target.') + if not hasattr(target, key): + raise errors.ModelUseError(f'Target {target} does not have key {key}. ' + f'So, it can not assign input to it.') + + # value and data type + val = input[2] + if isinstance(input[2], (int, float)): + data_type = 'fix' + else: + shape = backend.shape(val) + if shape[0] == run_length: + data_type = 'iter' + elif shape == target.size: + data_type = 'fix' + else: + raise errors.ModelUseError(f'Unknown size of input for "{key}", it should ' + f'either be {target.size}, nor be the shape ' + f'of {(run_length,) + target.size}') + + # operation + if len(input) == 4: + ops = input[3] + else: + ops = '+' + + # final result + if target_name not in formatted_inputs: + formatted_inputs[target_name] = [] + format_inp = (key, val, ops, data_type) + formatted_inputs[target_name].append(format_inp) + return formatted_inputs + diff --git a/brainpy/tools/__init__.py b/brainpy/tools/__init__.py index 9b54bad5..265c8ae1 100644 --- a/brainpy/tools/__init__.py +++ b/brainpy/tools/__init__.py @@ -3,4 +3,3 @@ from .ast2code import * from .codes import * from .dicts import * -from .functions import * diff --git a/brainpy/tools/ast2code.py b/brainpy/tools/ast2code.py index 79c204cb..5c2c43b7 100644 --- a/brainpy/tools/ast2code.py +++ b/brainpy/tools/ast2code.py @@ -10,8 +10,9 @@ import ast import sys from contextlib import contextmanager + __all__ = [ - 'ast2code' + 'ast2code', ] diff --git a/brainpy/tools/codes.py b/brainpy/tools/codes.py index 7ee79708..af679f3f 100644 --- a/brainpy/tools/codes.py +++ b/brainpy/tools/codes.py @@ -1,64 +1,30 @@ # -*- coding: utf-8 -*- import ast -import inspect import re from types import LambdaType +from brainpy import errors from .ast2code import ast2code -from .dicts import DictPlus -from ..errors import CodeError -from ..errors import DiffEquationError __all__ = [ - 'NoiseHandler', - - 'CodeLineFormatter', - 'format_code', - - 'LineFormatterForTrajectory', - 'format_code_for_trajectory', - - 'FindAtomicOp', - 'find_atomic_op', - - # replace function calls - 'replace_func', - 'FuncCallFinder', - - # string processing + # tools for code string 'get_identifiers', - 'get_main_code', - 'get_line_indent', - 'indent', 'deindent', 'word_replace', - # others + # other tools + 'NoiseHandler', + 'FindAtomicOp', + 'find_atomic_op', 'is_lambda_function', - - # - 'func_call', - 'get_func_source', ] -def is_lambda_function(func): - """Check whether the function is a ``lambda`` function. Comes from - https://stackoverflow.com/questions/23852423/how-to-check-that-variable-is-a-lambda-function - - Parameters - ---------- - func : callable function - The function. - - Returns - ------- - bool - True of False. - """ - return isinstance(func, LambdaType) and func.__name__ == "" +###################################### +# String tools +###################################### def get_identifiers(expr, include_numbers=False): @@ -102,121 +68,95 @@ def get_identifiers(expr, include_numbers=False): return (identifiers - _ID_KEYWORDS) | numbers -class NoiseHandler(object): - normal_pattern = re.compile(r'(_normal_like_)\((\w+)\)') - - @staticmethod - def vector_replace_f(m): - return 'numpy.random.normal(0., 1., ' + m.group(2) + '.shape)' - - @staticmethod - def scalar_replace_f(m): - return 'numpy.random.normal(0., 1.)' +def indent(text, num_tabs=1, spaces_per_tab=4, tab=None): + if tab is None: + tab = ' ' * spaces_per_tab + indent_ = tab * num_tabs + indented_string = indent_ + text.replace('\n', '\n' + indent_) + return indented_string - @staticmethod - def cuda_replace_f(m): - return 'xoroshiro128p_normal_float64(rng_states, _obj_i)' +def deindent(text, num_tabs=None, spaces_per_tab=4, docstring=False): + text = text.replace('\t', ' ' * spaces_per_tab) + lines = text.split('\n') + # if it's a docstring, we search for the common tabulation starting from + # line 1, otherwise we use all lines + if docstring: + start = 1 + else: + start = 0 + if docstring and len(lines) < 2: # nothing to do + return text + # Find the minimum indentation level + if num_tabs is not None: + indent_level = num_tabs * spaces_per_tab + else: + line_seq = [len(line) - len(line.lstrip()) for line in lines[start:] if len(line.strip())] + if len(line_seq) == 0: + indent_level = 0 + else: + indent_level = min(line_seq) + # remove the common indentation + lines[start:] = [line[indent_level:] for line in lines[start:]] + return '\n'.join(lines) -class FuncCallFinder(ast.NodeTransformer): - """""" +def word_replace(expr, substitutions): + """Applies a dict of word substitutions. - def __init__(self, func_name): - self.name = func_name - self.args = [] - self.kwargs = {} + The dict ``substitutions`` consists of pairs ``(word, rep)`` where each + word ``word`` appearing in ``expr`` is replaced by ``rep``. Here a 'word' + means anything matching the regexp ``\\bword\\b``. - def _get_attr_value(self, node, names): - if hasattr(node, 'value'): - names.insert(0, node.attr) - return self._get_attr_value(node.value, names) - else: - assert hasattr(node, 'id') - names.insert(0, node.id) - return names - - def visit_Call(self, node): - if getattr(node, 'starargs', None) is not None: - raise ValueError("Variable number of arguments not supported") - if getattr(node, 'kwargs', None) is not None: - raise ValueError("Keyword arguments not supported") - - if hasattr(node.func, 'id') and node.func.id == self.name: - for arg in node.args: - if isinstance(arg, ast.Name): - self.args.append(arg.id) - elif isinstance(arg, ast.Num): - self.args.append(arg.n) - else: - s = ast2code(ast.fix_missing_locations(arg)) - self.args.append(s.strip()) - for kv in node.keywords: - if isinstance(kv.value, ast.Name): - self.kwargs[kv.arg] = kv.value.id - elif isinstance(kv.value, ast.Num): - self.kwargs[kv.arg] = kv.value.n - else: - s = ast2code(ast.fix_missing_locations(kv.value)) - self.kwargs[kv.arg] = s.strip() - return ast.Name('_res') - else: - args = [self.visit(arg) for arg in node.args] - keywords = [self.visit(kv) for kv in node.keywords] - return ast.Call(func=node.func, args=args, keywords=keywords) + Examples + -------- + >>> expr = 'a*_b+c5+8+f(A)' + >>> print(word_replace(expr, {'a':'banana', 'f':'func'})) + banana*_b+c5+8+func(A) + """ + for var, replace_var in substitutions.items(): + # expr = re.sub(r'\b' + var + r'\b', str(replace_var), expr) + expr = re.sub(r'\b(?" - else: - func_codes = inspect.getsourcelines(func)[0] - idx = 0 - for i, line in enumerate(func_codes): - idx += 1 - line = line.replace(' ', '') - if '):' in line: - break - else: - code = "\n".join(func_codes) - raise ValueError(f'Can not parse function: \n{code}') - return ''.join(func_codes[idx:]) - else: - raise ValueError(f'Unknown function type: {type(func)}.') +class NoiseHandler(object): + normal_pattern = re.compile(r'(_normal_like_)\((\w+)\)') + + @staticmethod + def vector_replace_f(m): + return 'numpy.random.normal(0., 1., ' + m.group(2) + '.shape)' + + @staticmethod + def scalar_replace_f(m): + return 'numpy.random.normal(0., 1.)' -def get_line_indent(line, spaces_per_tab=4): - line = line.replace('\t', ' ' * spaces_per_tab) - return len(line) - len(line.lstrip()) + @staticmethod + def cuda_replace_f(m): + return 'xoroshiro128p_normal_float64(rng_states, _obj_i)' class FindAtomicOp(ast.NodeTransformer): @@ -230,7 +170,7 @@ class FindAtomicOp(ast.NodeTransformer): try: assert len(targets) == 1 except AssertionError: - raise DiffEquationError('Do not support multiple assignment.') + raise errors.DiffEqError('Do not support multiple assignment.') left = ast2code(ast.fix_missing_locations(targets[0])) key = targets[0].slice.value.s value = targets[0].value.id @@ -291,345 +231,3 @@ def find_atomic_op(code_line, var2idx): formatter = FindAtomicOp(var2idx) formatter.visit(tree) return formatter - - -class CodeLineFormatter(ast.NodeTransformer): - def __init__(self): - self.lefts = [] - self.rights = [] - self.lines = [] - self.scope = dict() - - def visit_Assign(self, node, level=0): - targets = node.targets - try: - assert len(targets) == 1 - except AssertionError: - raise DiffEquationError('Do not support multiple assignment.') - target = ast2code(ast.fix_missing_locations(targets[0])) - expr = ast2code(ast.fix_missing_locations(node.value)) - prefix = ' ' * level - self.lefts.append(target) - self.rights.append(expr) - self.lines.append(f'{prefix}{target} = {expr}') - return node - - def visit_AugAssign(self, node, level=0): - target = ast2code(ast.fix_missing_locations(node.target)) - op = ast2code(ast.fix_missing_locations(node.op)) - expr = ast2code(ast.fix_missing_locations(node.value)) - prefix = ' ' * level - self.lefts.append(target) - self.rights.append(f"{target} {op} {expr}") - self.lines.append(f"{prefix}{target} {op}= {expr}") - return node - - def visit_AnnAssign(self, node): - raise NotImplementedError('Do not support an assignment with a type annotation.') - - def visit_node_not_assign(self, node, level=0): - prefix = ' ' * level - expr = ast2code(ast.fix_missing_locations(node)) - self.lines.append(f'{prefix}{expr}') - - def visit_Assert(self, node, level=0): - self.visit_node_not_assign(node, level) - - def visit_Expr(self, node, level=0): - self.visit_node_not_assign(node, level) - - def visit_Expression(self, node, level=0): - self.visit_node_not_assign(node, level) - - def visit_content_in_condition_control(self, node, level): - if isinstance(node, ast.Expr): - self.visit_Expr(node, level) - elif isinstance(node, ast.Assert): - self.visit_Assert(node, level) - elif isinstance(node, ast.Assign): - self.visit_Assign(node, level) - elif isinstance(node, ast.AugAssign): - self.visit_AugAssign(node, level) - elif isinstance(node, ast.If): - self.visit_If(node, level) - elif isinstance(node, ast.For): - self.visit_For(node, level) - elif isinstance(node, ast.While): - self.visit_While(node, level) - else: - code = ast2code(ast.fix_missing_locations(node)) - raise CodeError(f'BrainPy does not support {type(node)}.\n\n{code}') - - def visit_If(self, node, level=0): - # If condition - prefix = ' ' * level - compare = ast2code(ast.fix_missing_locations(node.test)) - self.lines.append(f'{prefix}if {compare}:') - # body - for expr in node.body: - self.visit_content_in_condition_control(expr, level + 1) - - # elif - while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If): - node = node.orelse[0] - compare = ast2code(ast.fix_missing_locations(node.test)) - self.lines.append(f'{prefix}elif {compare}:') - for expr in node.body: - self.visit_content_in_condition_control(expr, level + 1) - - # else: - if len(node.orelse) > 0: - self.lines.append(f'{prefix}else:') - for expr in node.orelse: - self.visit_content_in_condition_control(expr, level + 1) - - def visit_For(self, node, level=0): - prefix = ' ' * level - # target - target = ast2code(ast.fix_missing_locations(node.target)) - # iter - iter = ast2code(ast.fix_missing_locations(node.iter)) - self.lefts.append(target) - self.rights.append(iter) - self.lines.append(prefix + f'for {target} in {iter}:') - # body - for expr in node.body: - self.visit_content_in_condition_control(expr, level + 1) - # else - if len(node.orelse) > 0: - self.lines.append(prefix + 'else:') - for expr in node.orelse: - self.visit_content_in_condition_control(expr, level + 1) - - def visit_While(self, node, level=0): - prefix = ' ' * level - # test - test = ast2code(ast.fix_missing_locations(node.test)) - self.rights.append(test) - self.lines.append(prefix + f'while {test}:') - # body - for expr in node.body: - self.visit_content_in_condition_control(expr, level + 1) - # else - if len(node.orelse) > 0: - self.lines.append(prefix + 'else:') - for expr in node.orelse: - self.visit_content_in_condition_control(expr, level + 1) - - def visit_Try(self, node): - raise CodeError('Do not support "try" handler.') - - def visit_With(self, node): - raise CodeError('Do not support "with" block.') - - def visit_Raise(self, node): - raise CodeError('Do not support "raise" statement.') - - def visit_Delete(self, node): - raise CodeError('Do not support "del" operation.') - - -def format_code(code_string): - """Get code lines from the string. - - Parameters - ---------- - code_string - - Returns - ------- - code_lines : list - """ - - tree = ast.parse(code_string.strip()) - formatter = CodeLineFormatter() - formatter.visit(tree) - return formatter - - -class LineFormatterForTrajectory(CodeLineFormatter): - def __init__(self, fixed_vars): - super(LineFormatterForTrajectory, self).__init__() - self.fixed_vars = fixed_vars - - def visit_Assign(self, node, level=0): - targets = node.targets - try: - assert len(targets) == 1 - except AssertionError: - raise DiffEquationError(f'Do not support multiple assignment. \n' - f'Error in code line: \n\n' - f'{ast2code(ast.fix_missing_locations(node))}') - prefix = ' ' * level - target = targets[0] - append_lines = [] - - if isinstance(target, ast.Subscript): - if target.value.id == 'ST' and target.slice.value.s in self.fixed_vars: - left = ast2code(ast.fix_missing_locations(target)) - self.lefts.append(left) - key = target.slice.value.s - self.lines.append(f'{prefix}{left} = _fixed_{key}') - self.scope[f'_fixed_{key}'] = self.fixed_vars[key] - return node - - elif hasattr(target, 'elts'): - if len(target.elts) == 1: - elt = target.elts[0] - if isinstance(elt, ast.Subscript): - if elt.value.id == 'ST' and elt.slice.value.s in self.fixed_vars: - left = ast2code(ast.fix_missing_locations(elt)) - self.lefts.append(left) - key = elt.slice.value.s - self.lines.append(f'{prefix}{left} = _fixed_{key}') - self.scope[f'_fixed_{key}'] = self.fixed_vars[key] - return node - left = ast2code(ast.fix_missing_locations(elt)) - expr = ast2code(ast.fix_missing_locations(node.value)) - self.lefts.append(left) - self.rights.append(expr) - self.lines.append(f'{prefix}{left} = {expr}') - return node - else: - for elt in target.elts: - if isinstance(elt, ast.Subscript): - if elt.value.id == 'ST' and elt.slice.value.s in self.fixed_vars: - left = ast2code(ast.fix_missing_locations(elt)) - key = elt.slice.value.s - line = f'{prefix}{left} = _fixed_{key}' - self.scope[f'_fixed_{key}'] = self.fixed_vars[key] - append_lines.append(line) - left = ast2code(ast.fix_missing_locations(target)) - expr = ast2code(ast.fix_missing_locations(node.value)) - self.lefts.append(target) - self.rights.append(expr) - self.lines.append(f'{prefix}{left} = {expr}') - self.lines.extend(append_lines) - return node - - left = ast2code(ast.fix_missing_locations(target)) - expr = ast2code(ast.fix_missing_locations(node.value)) - self.lefts.append(left) - self.rights.append(expr) - self.lines.append(f'{prefix}{left} = {expr}') - return node - - def visit_AugAssign(self, node, level=0): - prefix = ' ' * level - if isinstance(node.target, ast.Subscript): - if node.target.value.id == 'ST' and node.target.slice.value.s in self.fixed_vars: - left = ast2code(ast.fix_missing_locations(node.target)) - self.lefts.append(left) - key = node.target.slice.value.s - self.lines.append(f'{prefix}{left} = _fixed_{key}') - self.scope[f'_fixed_{key}'] = self.fixed_vars[key] - return node - - op = ast2code(ast.fix_missing_locations(node.op)) - left = ast2code(ast.fix_missing_locations(node.target)) - expr = ast2code(ast.fix_missing_locations(node.value)) - self.lefts.append(left) - self.rights.append(f"{left} {op} {expr}") - self.lines.append(f"{prefix}{left} {op}= {expr}") - return node - - -def format_code_for_trajectory(code_string, fixed_vars): - """Get _code lines from the string. - - Parameters - ---------- - code_string - - Returns - ------- - code_lines : list - """ - - tree = ast.parse(code_string.strip()) - formatter = LineFormatterForTrajectory(fixed_vars) - formatter.visit(tree) - return formatter - - -###################################### -# String tools -###################################### - - -def indent(text, num_tabs=1, spaces_per_tab=4, tab=None): - if tab is None: - tab = ' ' * spaces_per_tab - indent_ = tab * num_tabs - indented_string = indent_ + text.replace('\n', '\n' + indent_) - return indented_string - - -def deindent(text, num_tabs=None, spaces_per_tab=4, docstring=False): - text = text.replace('\t', ' ' * spaces_per_tab) - lines = text.split('\n') - # if it's a docstring, we search for the common tabulation starting from - # line 1, otherwise we use all lines - if docstring: - start = 1 - else: - start = 0 - if docstring and len(lines) < 2: # nothing to do - return text - # Find the minimum indentation level - if num_tabs is not None: - indent_level = num_tabs * spaces_per_tab - else: - line_seq = [len(line) - len(line.lstrip()) for line in lines[start:] if len(line.strip())] - if len(line_seq) == 0: - indent_level = 0 - else: - indent_level = min(line_seq) - # remove the common indentation - lines[start:] = [line[indent_level:] for line in lines[start:]] - return '\n'.join(lines) - - -def word_replace(expr, substitutions): - """Applies a dict of word substitutions. - - The dict ``substitutions`` consists of pairs ``(word, rep)`` where each - word ``word`` appearing in ``expr`` is replaced by ``rep``. Here a 'word' - means anything matching the regexp ``\\bword\\b``. - - Examples - -------- - - >>> expr = 'a*_b+c5+8+f(A)' - >>> print(word_replace(expr, {'a':'banana', 'f':'func'})) - banana*_b+c5+8+func(A) - """ - for var, replace_var in substitutions.items(): - # expr = re.sub(r'\b' + var + r'\b', str(replace_var), expr) - expr = re.sub(r'\b(?= self.Vth, self.V < self.Vth) + self.V = v + self.input = 0. + + +neurons = FitzHughNagumo(100, monitors=['V']) +neurons.run(300., inputs=('input', 1.), report=True) +bp.visualize.line_plot(neurons.mon.ts, neurons.mon.V, show=True) diff --git a/examples/hh_numba_cpu.py b/examples/hh_numba_cpu.py new file mode 100644 index 00000000..cc0410e7 --- /dev/null +++ b/examples/hh_numba_cpu.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + + +import numba as nb +import numpy as np + +import brainpy as bp + +bp.backend.set('numba') +bp.profile.set(dt=0.02) + + +class HH(bp.NeuGroup): + target_backend = ['numpy', 'numba'] + + def __init__(self, size, monitors, E_Na=50., E_K=-77., E_leak=-54.387, + C=1.0, g_Na=120., g_K=36., g_leak=0.03, V_th=20.): + self.E_Na = E_Na + self.E_K = E_K + self.E_leak = E_leak + self.C = C + self.g_Na = g_Na + self.g_K = g_K + self.g_leak = g_leak + self.V_th = V_th + + @nb.njit + @bp.odeint(method='rk4', show_code=True) + @nb.njit + def integral(V, m, h, n, t, Iext): + alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + beta = 4.0 * np.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * np.exp(-(V + 65) / 20.) + beta = 1 / (1 + np.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + + alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) + beta = 0.125 * np.exp(-(V + 65) / 80) + dndt = alpha * (1 - n) - beta * n + + I_Na = (g_Na * np.power(m, 3.0) * h) * (V - E_Na) + I_K = (g_K * np.power(n, 4.0)) * (V - E_K) + I_leak = g_leak * (V - E_leak) + dVdt = (- I_Na - I_K - I_leak + Iext) / C + + return dVdt, dmdt, dhdt, dndt + self.integral = integral + + self.V = np.ones(size) * -65. + self.m = np.ones(size) * 0.5 + self.h = np.ones(size) * 0.6 + self.n = np.ones(size) * 0.32 + self.spike = np.zeros(size) + self.input = np.zeros(size) + + super(HH, self).__init__( + size=size, + steps=[self.update], + monitors=monitors, + name='HH', + show_code=True, + ) + + def update(self, _t): + V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, self.input) + self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) + self.V = V + self.m = m + self.h = h + self.n = n + self.input = 0 + + +group = HH(100, monitors=['V']) +group.run(200., report=True) +bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) +group.run(200., inputs=('input', 10.), report=True) +bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5c000d82..fa393bff 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,9 @@ -r requirements.txt +sympy +numba +pytorch +tensorflow + # test requirements pytest diff --git a/requirements.txt b/requirements.txt index 40fa2205..9f0d4869 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ numpy>=1.13 -sympy>=1.2 -scipy>=1.2.0 -numba>=0.50 matplotlib>=3.0 diff --git a/setup.py b/setup.py index f9bf5614..803ed787 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import re from setuptools import find_packages from setuptools import setup -# obtain version string from __init__.py +# version here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 'brainpy', '__init__.py'), 'r') as f: init_py = f.read() @@ -14,8 +14,7 @@ version = re.search('__version__ = "(.*)"', init_py).groups()[0] # obtain long description from README and CHANGES README = ''' -``BrainPy`` is a lightweight framework based on the latest Just-In-Time (JIT) -compilers (especially `Numba `_). +``BrainPy`` is a unified framework for computational neuroscience and brain-inspired computation. The goal of ``BrainPy`` is to provide a unified simulation and analysis framework for neuronal analysis with the feature of high flexibility and efficiency. BrainPy is flexible because it endows the users with the fully data/logic flow control. @@ -26,31 +25,37 @@ BrainPy is efficient because it supports JIT acceleration on CPUs and GPUs. setup( name='brainpy-simulator', version=version, - description='BrainPy: A Toolbox for Computational Neuroscience Study and Research', + description='BrainPy: A unified toolbox for computational neuroscience and brain-inspired computation', long_description=README, author='Chaoming Wang', author_email='adaduo@outlook.com', packages=find_packages(exclude=['examples*', 'docs*', 'develop*', 'tests*']), - python_requires='>=3.7', + python_requires='>=3.6', install_requires=[ 'numpy>=1.15', - 'sympy>=1.2', - 'scipy>=1.2.0', - 'numba>=0.50.0', 'matplotlib>=3.0', ], url='https://github.com/PKU-NIP-Lab/BrainPy', - keywords='computational neuroscience', + keywords='computational neuroscience, ' + 'dynamical systems, ' + 'differential equations, ' + 'numerical integration, ' + 'ordinary differential equations, ' + 'stochastic differential equations, ' + 'delay differential equations, ' + 'fractional differential equations, ' + 'ODE, SDE, DDE, FDE', classifiers=[ - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Topic :: Scientific/Engineering :: Bio-Informatics', 'Topic :: Scientific/Engineering :: Mathematics', 'Topic :: Scientific/Engineering :: Artificial Intelligence', diff --git a/tests/backend/__init__.py b/tests/backend/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/tests/backend/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/backend/runners/__init__.py b/tests/backend/runners/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/tests/backend/runners/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/backend/runners/numba_runner.py b/tests/backend/runners/numba_runner.py new file mode 100644 index 00000000..34dc37f0 --- /dev/null +++ b/tests/backend/runners/numba_runner.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +from brainpy.backend.runners.numba_cpu_runner import analyze_step_func + + +import numpy as np + +import brainpy as bp + +bp.backend.set('numpy') + + +class HH(bp.NeuGroup): + target_backend = ['numpy'] + + def __init__(self, size, monitors=None, E_Na=50., E_K=-77., E_leak=-54.387, C=1.0, + g_Na=120., g_K=36., g_leak=0.03, V_th=20.): + + @bp.odeint(method='rkdp', show_code=False) + def integral(V, m, h, n, t, Iext): + alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + beta = 4.0 * np.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * np.exp(-(V + 65) / 20.) + beta = 1 / (1 + np.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + + alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) + beta = 0.125 * np.exp(-(V + 65) / 80) + dndt = alpha * (1 - n) - beta * n + + I_Na = (g_Na * np.power(m, 3.0) * h) * (V - E_Na) + I_K = (g_K * np.power(n, 4.0)) * (V - E_K) + I_leak = g_leak * (V - E_leak) + dVdt = (- I_Na - I_K - I_leak + Iext) / C + + return dVdt, dmdt, dhdt, dndt + + self.E_Na = E_Na + self.E_K = E_K + self.E_leak = E_leak + self.C = C + self.g_Na = g_Na + self.g_K = g_K + self.g_leak = g_leak + self.V_th = V_th + + self.integral = integral + + self.V = np.ones(size) * -65. + self.m = np.ones(size) * 0.5 + self.h = np.ones(size) * 0.6 + self.n = np.ones(size) * 0.32 + self.spike = np.zeros(size) + self.input = np.zeros(size) + + super(HH, self).__init__(size=size, + steps=[self.update], + monitors=monitors, + name='HH') + + def update(self, _t): + V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, self.input) + # m = np.clip(m, 0., 1.) + # h = np.clip(h, 0., 1.) + # n = np.clip(n, 0., 1.) + self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) + self.V = V + self.m = m + self.h = h + self.n = n + self.input = 0. + + +def test_analyze_step(): + group = HH(100, ['V']) + r = analyze_step_func(group.update) + + print('Code of the function:') + print(r[0]) + print('Code Scope:') + print(r[1]) + print('Data need pass:') + print(r[2]) + print('Data need return:') + print(r[3]) + + +test_analyze_step() diff --git a/tests/integration/test_diff_equation.py b/tests/integration/test_diff_equation.py index e33ca229..cfa8f6e7 100644 --- a/tests/integration/test_diff_equation.py +++ b/tests/integration/test_diff_equation.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import numpy as np -from brainpy.integration import DiffEquation +from brainpy.integrators import DiffEquation from brainpy import integrate diff --git a/tests/integration/test_integrators.py b/tests/integration/test_integrators.py index 641437fc..da448391 100644 --- a/tests/integration/test_integrators.py +++ b/tests/integration/test_integrators.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- import numpy as np -from brainpy.integration import integrate -from brainpy.integration import integrator +from brainpy.integrators import integrate +from brainpy.integrators.sde import integrator def test_exponential_euler_by_linear_system(): diff --git a/tests/integration/test_utils.py b/tests/integration/test_utils.py index d3d88a9b..f5654c40 100644 --- a/tests/integration/test_utils.py +++ b/tests/integration/test_utils.py @@ -2,8 +2,8 @@ import pytest -from brainpy.errors import DiffEquationError -from brainpy.integration import utils +from brainpy.errors import DiffEqError +from brainpy.integrators import ast_analysis def try_diff_eq_analyser(): @@ -14,7 +14,7 @@ beta = 4.0 * np.exp(-(V + 65) / 18) return alpha * (1 - m) - beta * m, f(alpha, beta) ''' - res = utils.analyse_diff_eq(code) + res = ast_analysis.analyse_diff_eq(code) assert res.return_intermediates == [] assert res.return_type == 'x,x' @@ -32,38 +32,38 @@ return alpha * (1 - m) - beta * m, f(alpha, beta) def try_diff_eq_analyser2(): - res = utils.analyse_diff_eq('return a') + res = ast_analysis.analyse_diff_eq('return a') assert len(res.return_intermediates) == 0 assert res.return_type == 'x' assert res.f_expr[1] == 'a' assert res.g_expr is None - res = utils.analyse_diff_eq('return a, b') + res = ast_analysis.analyse_diff_eq('return a, b') assert len(res.return_intermediates) == 0 assert res.return_type == 'x,x' assert res.f_expr[1] == 'a' assert res.g_expr[1] == 'b' - res = utils.analyse_diff_eq('return (a, b)') + res = ast_analysis.analyse_diff_eq('return (a, b)') assert len(res.return_intermediates) == 0 assert res.return_type == 'x,x' assert res.f_expr[1] == 'a' assert res.g_expr[1] == 'b' - res = utils.analyse_diff_eq('return (a, ), b') + res = ast_analysis.analyse_diff_eq('return (a, ), b') assert len(res.return_intermediates) == 1 assert res.return_intermediates[0] == 'b' assert res.return_type == '(x,),' assert res.f_expr[1] == 'a' assert res.g_expr is None - res = utils.analyse_diff_eq('return (a, b), ') + res = ast_analysis.analyse_diff_eq('return (a, b), ') assert len(res.return_intermediates) == 0 assert res.return_type == '(x,x),' assert res.f_expr[1] == 'a' assert res.g_expr[1] == 'b' - res = utils.analyse_diff_eq('return (a, b), c, d') + res = ast_analysis.analyse_diff_eq('return (a, b), c, d') assert len(res.return_intermediates) == 2 assert res.return_intermediates[0] == 'c' assert res.return_intermediates[1] == 'd' @@ -71,7 +71,7 @@ def try_diff_eq_analyser2(): assert res.f_expr[1] == 'a' assert res.g_expr[1] == 'b' - res = utils.analyse_diff_eq('return ((a, b), c, d)') + res = ast_analysis.analyse_diff_eq('return ((a, b), c, d)') assert len(res.return_intermediates) == 2 assert res.return_intermediates[0] == 'c' assert res.return_intermediates[1] == 'd' @@ -79,19 +79,19 @@ def try_diff_eq_analyser2(): assert res.f_expr[1] == 'a' assert res.g_expr[1] == 'b' - res = utils.analyse_diff_eq('return (a, ), ') + res = ast_analysis.analyse_diff_eq('return (a, ), ') assert len(res.return_intermediates) == 0 assert res.return_type == '(x,),' assert res.f_expr[1] == 'a' assert res.g_expr is None - res = utils.analyse_diff_eq('return (a+b, b*2), ') + res = ast_analysis.analyse_diff_eq('return (a+b, b*2), ') assert len(res.return_intermediates) == 0 assert res.return_type == '(x,x),' assert res.f_expr[1] == 'a + b' assert res.g_expr[1] == 'b * 2' - with pytest.raises(DiffEquationError) as e: - utils.analyse_diff_eq('return a, b, c') + with pytest.raises(DiffEqError) as e: + ast_analysis.analyse_diff_eq('return a, b, c') diff --git a/tests/integrators/__init__.py b/tests/integrators/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/tests/integrators/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/integrators/test_ast_analysis.py b/tests/integrators/test_ast_analysis.py new file mode 100644 index 00000000..9ea10990 --- /dev/null +++ b/tests/integrators/test_ast_analysis.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +import ast +from pprint import pprint + +import pytest + +from brainpy.errors import DiffEqError +from brainpy.integrators.ast_analysis import DiffEqReader +from brainpy.integrators.ast_analysis import separate_variables + + +def test_reader1(): + eq_code = ''' +def func(): + a, b = f() + c = a + b + d = a * b + return c, d + ''' + analyser = DiffEqReader() + analyser.visit(ast.parse(eq_code)) + + print(analyser.returns) + assert analyser.returns == ['c', 'd'] + + print(analyser.code_lines) + assert analyser.code_lines == ['a, b = f()\n', + 'c = a + b\n', + 'd = a * b\n'] + + print(analyser.rights) + assert analyser.rights == ['f()', 'a + b', 'a * b'] + + print(analyser.variables) + assert analyser.variables == [['a', 'b'], + ['c'], + ['d']] + + +def test_reader2(): + eq_code = ''' +def func(): + a, b = f() + if a > 0: + c = a + b + else: + c = a * b + return c + ''' + analyser = DiffEqReader() + with pytest.raises(DiffEqError): + analyser.visit(ast.parse(eq_code)) + + +def test_reader3(): + eq_code = ''' +def func(): + a, b = f() + for i in range(10): + a += b + return a + ''' + analyser = DiffEqReader() + with pytest.raises(DiffEqError): + analyser.visit(ast.parse(eq_code)) + + +def test_separate_variables1(): + eq_code = ''' +def integral(V, m, h, n, t, Iext): + alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + beta = 4.0 * np.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * np.exp(-(V + 65) / 20.) + beta = 1 / (1 + np.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + return dmdt, dhdt +''' + analyser = DiffEqReader() + analyser.visit(ast.parse(eq_code)) + + print('returns: ') + pprint(analyser.returns) + + print("code_lines: ") + pprint(analyser.code_lines) + + print("rights: ") + pprint(analyser.rights) + + print("variables: ") + pprint(analyser.variables) + + r = separate_variables(returns=analyser.returns, + variables=analyser.variables, + right_exprs=analyser.rights, + code_lines=analyser.code_lines) + pprint(r) + + + +# test_separate_variables1() diff --git a/tests/integrators/test_ode_adaptive_rk.py b/tests/integrators/test_ode_adaptive_rk.py new file mode 100644 index 00000000..54e7bf1a --- /dev/null +++ b/tests/integrators/test_ode_adaptive_rk.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import numba +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +from integrators import ode + + +sigma = 10 +beta = 8 / 3 +rho = 28 + + +@numba.njit +def lorenz_f(x, y, z, t): + dx = sigma * (y - x) + dy = x * (rho - z) - y + dz = x * y - beta * z + return dx, dy, dz + + +def lorenz_system(method, dt=0.01, tol=0.1): + integral = numba.njit(method(lorenz_f, show_code=True, tol=tol, adaptive=True)) + + times = np.arange(0, 100, 0.01) + mon1 = [] + mon2 = [] + mon3 = [] + mon4 = [] + x, y, z = 1, 1, 1 + for t in times: + x, y, z, dt = integral(x, y, z, t, dt) + mon1.append(x) + mon2.append(y) + mon3.append(z) + mon4.append(dt) + mon1 = np.array(mon1) + mon2 = np.array(mon2) + mon3 = np.array(mon3) + mon4 = np.array(mon4) + + fig = plt.figure() + ax = fig.gca(projection='3d') + plt.plot(mon1, mon2, mon3) + ax.set_xlabel('x') + ax.set_xlabel('y') + ax.set_xlabel('z') + + fig = plt.figure() + plt.plot(mon4) + + plt.show() + + +lorenz_system(ode.rkf45, dt=0.1, tol=0.001) + +if __name__ == '__main__': + Axes3D diff --git a/tests/integrators/test_ode_rk.py b/tests/integrators/test_ode_rk.py new file mode 100644 index 00000000..c1b28513 --- /dev/null +++ b/tests/integrators/test_ode_rk.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import numpy as np +from brainpy.integrators import ode +import numba +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D + + +sigma = 10 +beta = 8 / 3 +rho = 28 + + +@numba.njit +def lorenz_f(x, y, z, t): + dx = sigma * (y - x) + dy = x * (rho - z) - y + dz = x * y - beta * z + return dx, dy, dz + + +def lorenz_system(method): + integral = numba.njit(method(lorenz_f, show_code=True, dt=0.005)) + + times = np.arange(0, 100, 0.01) + mon1 = [] + mon2 = [] + mon3 = [] + x, y, z = 1, 1, 1 + for t in times: + x, y, z = integral(x, y, z, t) + mon1.append(x) + mon2.append(y) + mon3.append(z) + mon1 = np.array(mon1) + mon2 = np.array(mon2) + mon3 = np.array(mon3) + + fig = plt.figure() + ax = fig.gca(projection='3d') + plt.plot(mon1, mon2, mon3) + ax.set_xlabel('x') + ax.set_xlabel('y') + ax.set_xlabel('z') + plt.show() + + +lorenz_system(ode.rk4) + + +if __name__ == '__main__': + Axes3D diff --git a/tests/integrators/test_sde_scalar.py b/tests/integrators/test_sde_scalar.py new file mode 100644 index 00000000..3b0d80ad --- /dev/null +++ b/tests/integrators/test_sde_scalar.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +import matplotlib.pyplot as plt +import numba +import numpy as np +from mpl_toolkits.mplot3d import Axes3D + + +import brainpy as bp +bp.backend.set('numba') +from brainpy.integrators import sde + + +sigma = 10 +beta = 8 / 3 +rho = 28 +p = 0.1 + + +@numba.njit +def lorenz_f(x, y, z, t): + dx = sigma * (y - x) + dy = x * (rho - z) - y + dz = x * y - beta * z + return dx, dy, dz + + +@numba.njit +def lorenz_g(x, y, z, t): + return p * x, p * y, p * z + + +def lorenz_system(method, **kwargs): + integral = numba.njit(method(f=lorenz_f, g=lorenz_g, show_code=True, dt=0.005, + **kwargs)) + + times = np.arange(0, 100, 0.01) + mon1 = [] + mon2 = [] + mon3 = [] + x, y, z = 1, 1, 1 + for t in times: + x, y, z = integral(x, y, z, t) + mon1.append(x) + mon2.append(y) + mon3.append(z) + mon1 = np.array(mon1) + mon2 = np.array(mon2) + mon3 = np.array(mon3) + + fig = plt.figure() + ax = fig.gca(projection='3d') + plt.plot(mon1, mon2, mon3) + ax.set_xlabel('x') + ax.set_xlabel('y') + ax.set_xlabel('z') + plt.show() + + +# lorenz_system(sde.srk1w1_scalar) +# lorenz_system(sde.srk2w1_scalar) +# lorenz_system(sde.euler, sde_type=bp.integrators.ITO_SDE) +# lorenz_system(sde.euler, sde_type=bp.integrators.STRA_SDE) +# lorenz_system(sde.milstein, sde_type=bp.integrators.ITO_SDE) +# lorenz_system(sde.milstein, sde_type=bp.integrators.STRA_SDE) +lorenz_system(sde.srk1_strong, + sde_type=bp.integrators.STRA_SDE, + wiener_type=bp.integrators.SCALAR_WIENER, + var_type=bp.integrators.POPU_VAR) + + +if __name__ == '__main__': + Axes3D diff --git a/tests/test_dynamics.py b/tests/test_dynamics.py index 04add017..f125be2f 100644 --- a/tests/test_dynamics.py +++ b/tests/test_dynamics.py @@ -10,7 +10,7 @@ import sympy # beta = 4.0 * np.exp(-(V + 65) / 18) # return alpha * (1 - m) - beta * m, (alpha, beta) # -# sympy_eqs = [ bp.integration.str2sympy(str(eq)) +# sympy_eqs = [ bp.integrators.str2sympy(str(eq)) # for eq in int_m.diff_eq.get_f_expressions()] @@ -23,7 +23,7 @@ f = sympy.Function('f') alpha = 0.1 * (V + 40) / (1 - sympy.exp(-(V + 40) / 10)) beta = 4.0 * sympy.exp(-(V + 65) / 18) dvdt = f(alpha) * (1 - m) - beta * m -# dvdt = bp.integration.str2sympy('alpha * (1 - m) - beta * m') +# dvdt = bp.integrators.str2sympy('alpha * (1 - m) - beta * m') # print(sympy.Derivative(dvdt, V).doit()) diff = sympy.diff(dvdt, V) diff --git a/tests/test_neugroup.py b/tests/test_neugroup.py index 90d461ff..446aaf1c 100644 --- a/tests/test_neugroup.py +++ b/tests/test_neugroup.py @@ -1,5 +1,5 @@ -from brainpy.core import types -from brainpy.core.neurons import NeuGroup, NeuType +from brainpy.simulation import types +from brainpy.simulation.model_neuron import NeuGroup, NeuType import numpy as np from brainpy import profile diff --git a/tests/test_ode_adaptive_rk.py b/tests/test_ode_adaptive_rk.py new file mode 100644 index 00000000..fa7e6d8d --- /dev/null +++ b/tests/test_ode_adaptive_rk.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import numba +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +from brainpy.integrators import ode + +Axes3D + +sigma = 10 +beta = 8 / 3 +rho = 28 + + +@numba.njit +def lorenz_f(x, y, z, t): + dx = sigma * (y - x) + dy = x * (rho - z) - y + dz = x * y - beta * z + return dx, dy, dz + + +def lorenz_system(method, dt=0.01, tol=0.1): + integral = numba.njit(method(lorenz_f, show_code=True, tol=tol, adaptive=True)) + + times = np.arange(0, 100, 0.01) + mon1 = [] + mon2 = [] + mon3 = [] + mon4 = [] + x, y, z = 1, 1, 1 + for t in times: + x, y, z, dt = integral(x, y, z, t, dt) + mon1.append(x) + mon2.append(y) + mon3.append(z) + mon4.append(dt) + mon1 = np.array(mon1) + mon2 = np.array(mon2) + mon3 = np.array(mon3) + mon4 = np.array(mon4) + + fig = plt.figure() + ax = fig.gca(projection='3d') + plt.plot(mon1, mon2, mon3) + ax.set_xlabel('x') + ax.set_xlabel('y') + ax.set_xlabel('z') + + fig = plt.figure() + plt.plot(mon4) + + plt.show() + + +lorenz_system(ode.rkf45, dt=0.1, tol=0.001) + + diff --git a/tests/test_ode_rk.py b/tests/test_ode_rk.py new file mode 100644 index 00000000..de04fea4 --- /dev/null +++ b/tests/test_ode_rk.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import numpy as np +from brainpy.integrators import ode +import numba +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D + + +Axes3D + + +sigma = 10 +beta = 8 / 3 +rho = 28 + + +@numba.njit +def lorenz_f(x, y, z, t): + dx = sigma * (y - x) + dy = x * (rho - z) - y + dz = x * y - beta * z + return dx, dy, dz + + +def lorenz_system(method): + integral = numba.njit(method(lorenz_f, show_code=True, dt=0.005)) + + times = np.arange(0, 100, 0.01) + mon1 = [] + mon2 = [] + mon3 = [] + x, y, z = 1, 1, 1 + for t in times: + x, y, z = integral(x, y, z, t) + mon1.append(x) + mon2.append(y) + mon3.append(z) + mon1 = np.array(mon1) + mon2 = np.array(mon2) + mon3 = np.array(mon3) + + fig = plt.figure() + ax = fig.gca(projection='3d') + plt.plot(mon1, mon2, mon3) + ax.set_xlabel('x') + ax.set_xlabel('y') + ax.set_xlabel('z') + plt.show() + + +lorenz_system(ode.rk4) + diff --git a/tests/test_runner_gpu.py b/tests/test_runner_gpu.py index c3ad6fb5..b5cb8e9b 100644 --- a/tests/test_runner_gpu.py +++ b/tests/test_runner_gpu.py @@ -6,7 +6,7 @@ import numpy as np from numba import cuda import brainpy as bp -from brainpy.core.runner import Runner +from brainpy.backend.runners.numba_cpu_runner import NumbaCPUNodeRunner def define_lif(): @@ -51,9 +51,9 @@ def test_input_fix(): lif = define_lif() num = 100 - group = bp.NeuGroup(lif, geometry=(num,)) + group = bp.NeuGroup(lif, size=(num,)) - runner = Runner(group) + runner = NumbaCPUNodeRunner(group) res = runner.get_codes_of_input([('ST.input', 1., '=', 'fix')]) assert res['input-0']['num_data'] == num assert res['input-0']['codes'][-1].endswith('ST_input_inp') @@ -62,7 +62,7 @@ def test_input_fix(): print('\n' * 3) - runner = Runner(group) + runner = NumbaCPUNodeRunner(group) res = runner.get_codes_of_input([('ST.input', np.random.random(100), '=', 'fix')]) assert res['input-0']['num_data'] == num assert res['input-0']['codes'][-1].endswith('ST_input_inp[cuda_i]') @@ -77,9 +77,9 @@ def test_input_iter(): bp.profile.set(jit=True, device='gpu') lif = define_lif() num = 100 - group = bp.NeuGroup(lif, geometry=(num,)) + group = bp.NeuGroup(lif, size=(num,)) - runner = Runner(group) + runner = NumbaCPUNodeRunner(group) res = runner.get_codes_of_input([('ST.input', np.random.random(1000), '=', 'iter')]) assert res['input-0']['num_data'] == num assert res['input-0']['codes'][-1].endswith('ST_input_inp[_i]') @@ -87,7 +87,7 @@ def test_input_iter(): print('\n' * 3) - runner = Runner(group) + runner = NumbaCPUNodeRunner(group) res = runner.get_codes_of_input([('ST.input', np.random.random((1000, num)), '=', 'iter')]) assert res['input-0']['num_data'] == num assert res['input-0']['codes'][-1].endswith('ST_input_inp[_i, cuda_i]') @@ -103,9 +103,9 @@ def test_monitor(): lif = define_lif() num = 100 - group = bp.NeuGroup(lif, geometry=(num,), monitors=['spike']) + group = bp.NeuGroup(lif, size=(num,), monitors=['spike']) - runner = Runner(group) + runner = NumbaCPUNodeRunner(group) mon, res = runner.get_codes_of_monitor([('ST.spike', None)], 1000) assert res['monitor-0']['num_data'] == num assert res['monitor-0']['codes'][-1].endswith('ST[2, cuda_i]') @@ -113,7 +113,7 @@ def test_monitor(): pprint(res) print('\n' * 4) - runner = Runner(group) + runner = NumbaCPUNodeRunner(group) mon, res = runner.get_codes_of_monitor([('ST.spike', [1, 2, 4])], 1000) assert res['monitor-0']['num_data'] == 3 assert res['monitor-0']['codes'][-1].endswith('= ST[2, mon_idx]') @@ -129,9 +129,9 @@ def test_neuron_steps(): lif = define_lif() num = 100 - group = bp.NeuGroup(lif, geometry=(num,)) + group = bp.NeuGroup(lif, size=(num,)) - runner = Runner(group) + runner = NumbaCPUNodeRunner(group) res = runner.step_scalar_model() pprint(res) # assert res['monitor-0']['num_data'] == num diff --git a/tests/test_trajectoty_runner.py b/tests/test_trajectoty_runner.py index f69a04ef..670d122b 100644 --- a/tests/test_trajectoty_runner.py +++ b/tests/test_trajectoty_runner.py @@ -4,7 +4,7 @@ from pprint import pprint import brainpy as bp import numpy as np -from brainpy.core.runner import TrajectoryRunner +from brainpy.backend.runners.numba_cpu_runner import TrajectoryNumbaRunner import matplotlib.pyplot as plt bp.profile.set(jit=True, show_code=True) @@ -216,9 +216,9 @@ Izhikevich = bp.NeuType(name='Izhikevich', requires={'ST': state}, steps=update, if __name__ == '__main__1': - group = bp.NeuGroup(HH, geometry=10) + group = bp.NeuGroup(HH, size=10) - runner = TrajectoryRunner(group, target_vars=['m', 'h']) + runner = TrajectoryNumbaRunner(group, target_vars=['m', 'h']) print(runner.target_vars) print(runner.fixed_vars) @@ -281,7 +281,7 @@ def get_trajectories( group = NeuGroup(model, geometry=num, monitors=target_vars, pars_update=pars_update) for j, key in enumerate(target_vars): group.ST[key] = initial_states[j] - group.runner = TrajectoryRunner(group, target_vars=target_vars, fixed_vars=fixed_vars) + group.runner = TrajectoryNumbaRunner(group, target_vars=target_vars, fixed_vars=fixed_vars) group.run(duration=duration, inputs=inputs) # monitors -- 2.34.1 From 28b8a26255ca1c27201ba01183e6858b76a92b7a Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Sun, 21 Mar 2021 16:56:03 +0800 Subject: [PATCH 04/15] Support NumbaCPU runner, support dynamics analysis --- brainpy/__init__.py | 36 +- brainpy/analysis/__init__.py | 6 +- brainpy/analysis/base.py | 153 ++++---- brainpy/analysis/bifurcation.py | 114 +++--- brainpy/analysis/dyn_model.py | 84 +++++ brainpy/analysis/phase_plane.py | 111 +++--- brainpy/analysis/stability.py | 57 ++- brainpy/analysis/utils.py | 36 +- brainpy/backend/__init__.py | 42 ++- brainpy/backend/operators/bk_numba_cpu.py | 19 +- brainpy/backend/operators/bk_numba_cuda.py | 2 - brainpy/backend/operators/bk_numpy.py | 3 + brainpy/backend/operators/bk_pytorch.py | 3 +- brainpy/backend/operators/standard.py | 11 +- brainpy/backend/runners/general_runner.py | 8 +- brainpy/backend/runners/jax_runner.py | 2 +- brainpy/backend/runners/numba_cpu_runner.py | 352 +++++++++++++----- brainpy/backend/runners/numba_cuda_runner.py | 147 +++++++- brainpy/backend/runners/utils.py | 6 +- brainpy/connectivity/base.py | 25 +- brainpy/connectivity/methods.py | 13 + brainpy/inputs.py | 8 +- brainpy/integrators/__init__.py | 6 +- brainpy/integrators/ast_analysis.py | 75 ++-- brainpy/integrators/delay_vars.py | 53 +-- brainpy/integrators/integrate_wrapper.py | 104 ++++-- brainpy/integrators/ode/exp_euler.py | 3 +- .../integrators/ode/rk_adaptive_methods.py | 8 +- brainpy/integrators/ode/rk_methods.py | 40 +- brainpy/integrators/ode/wrapper.py | 115 ++++-- brainpy/integrators/sde/common.py | 18 +- brainpy/integrators/sde/euler_and_milstein.py | 15 +- brainpy/integrators/sde/exp_euler.py | 13 +- brainpy/integrators/sde/srk_scalar.py | 21 +- brainpy/integrators/sde/srk_strong.py | 5 +- brainpy/integrators/sympy_analysis.py | 234 +++--------- brainpy/integrators/utils.py | 27 +- brainpy/measure.py | 4 +- brainpy/profile.py | 78 ---- brainpy/simulation/__init__.py | 2 +- brainpy/simulation/delay.py | 62 +++ brainpy/simulation/network.py | 8 +- brainpy/simulation/population.py | 148 +++++--- brainpy/simulation/runner.py | 10 +- brainpy/simulation/utils.py | 24 +- brainpy/tools/codes.py | 145 +++----- brainpy/visualization/figures.py | 14 +- brainpy/visualization/plots.py | 6 +- examples/FitzHugh_Nagumo.py | 49 --- examples/hh_numba_cpu.py | 80 ---- examples/neurons/FitzHugh_Nagumo.py | 61 +++ examples/neurons/HH_model.py | 76 ++++ examples/synapses/AMPA_synapse.py | 158 ++++++++ tests/backend/runners/numba_cpu_runner.py | 280 ++++++++++++++ tests/backend/runners/numba_runner.py | 90 ----- 55 files changed, 2010 insertions(+), 1260 deletions(-) create mode 100644 brainpy/analysis/dyn_model.py delete mode 100644 brainpy/profile.py create mode 100644 brainpy/simulation/delay.py delete mode 100644 examples/FitzHugh_Nagumo.py delete mode 100644 examples/hh_numba_cpu.py create mode 100644 examples/neurons/FitzHugh_Nagumo.py create mode 100644 examples/neurons/HH_model.py create mode 100644 examples/synapses/AMPA_synapse.py create mode 100644 tests/backend/runners/numba_cpu_runner.py delete mode 100644 tests/backend/runners/numba_runner.py diff --git a/brainpy/__init__.py b/brainpy/__init__.py index 61c44cbd..e073d202 100644 --- a/brainpy/__init__.py +++ b/brainpy/__init__.py @@ -1,43 +1,33 @@ # -*- coding: utf-8 -*- -__version__ = "1.0.0-rc1" +__version__ = "1.0.0-alpha" -# "profile" module -from . import profile +# "analysis" module +from . import analysis # "backend" module from . import backend +# "connectivity" module +from . import connectivity +from . import connectivity as connect + +# "simulation" module +from . import simulation +from .simulation.population import * +from .simulation.network import * + # "integrators" module from . import integrators from .integrators import ode from .integrators import sde from .integrators.integrate_wrapper import * -from .integrators.delay_vars import ConstantDelay -from .integrators.delay_vars import VaryingDelay -from .integrators.delay_vars import NeutralDelay - -# "simulation" module -from . import simulation as core -from .simulation.population import Population -from .simulation.population import NeuGroup -from .simulation.population import TwoEndConn -from .simulation.network import Network - -# "connectivity" module -from . import connectivity -from . import connectivity as connect - -# "analysis" module -from . import analysis # "visualization" module from . import visualization as visualize -# "tools" module -from . import tools - # other modules +from . import tools from . import inputs from . import measure from . import running diff --git a/brainpy/analysis/__init__.py b/brainpy/analysis/__init__.py index a322388b..378af970 100644 --- a/brainpy/analysis/__init__.py +++ b/brainpy/analysis/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from .solver import * -from .utils import * from .base import * -from .phase_plane import * from .bifurcation import * +from .phase_plane import * +from .solver import * +from .utils import * diff --git a/brainpy/analysis/base.py b/brainpy/analysis/base.py index 445e7537..680b57b1 100644 --- a/brainpy/analysis/base.py +++ b/brainpy/analysis/base.py @@ -6,13 +6,12 @@ from copy import deepcopy import numpy as np import sympy -from brainpy import simulation from brainpy import errors -from brainpy import integrators from brainpy import tools +from brainpy.analysis import dyn_model from brainpy.analysis import solver from brainpy.analysis import utils - +from brainpy.integrators import sympy_analysis __all__ = [ 'BaseNeuronAnalyzer', @@ -36,7 +35,7 @@ class BaseNeuronAnalyzer(object): Parameters ---------- - model_or_intgs : simulation.Population, function, functions + model_or_integrals : simulation.Population, function, functions A model of the population, the integrator function, or a list/tuple of integrator functions. target_vars : dict @@ -73,7 +72,7 @@ class BaseNeuronAnalyzer(object): """ def __init__(self, - model_or_intgs, + model_or_integrals, target_vars, fixed_vars=None, target_pars=None, @@ -83,10 +82,13 @@ class BaseNeuronAnalyzer(object): # model # ----- - if not isinstance(model_or_intgs, simulation.Population): - raise errors.ModelUseError(f'Neuron Dynamics Analyzer now only support Population, ' - f'but get {type(model_or_intgs)}.') - self.model = model_or_intgs + if isinstance(model_or_integrals, dyn_model.DynamicalModel): + self.model = model_or_integrals + elif (isinstance(model_or_integrals, (tuple, list)) and callable(model_or_integrals[0])) or \ + callable(model_or_integrals): + self.model = dyn_model.transform_integrals_to_analyzers(model_or_integrals) + else: + raise ValueError # target variables # ---------------- @@ -98,43 +100,40 @@ class BaseNeuronAnalyzer(object): self.dvar_names = list(self.target_vars.keys()) else: self.dvar_names = list(sorted(self.target_vars.keys())) + for key in self.target_vars.keys(): + if key not in self.model.variables: + raise ValueError(f'{key} is not a dynamical variable in {self.model}.') # fixed variables # ---------------- - if fixed_vars is None: - fixed_vars = dict() if not isinstance(fixed_vars, dict): raise errors.ModelUseError('"fixed_vars" must be a dict with the format ' 'of {"var1": val1, "var2": val2}.') - self.fixed_vars = dict() - for integrator in model_or_intgs.integrators: - var_name = integrator.diff_eq.var_name - if var_name not in target_vars: - if var_name in fixed_vars: - self.fixed_vars[var_name] = fixed_vars.get(var_name) - else: - self.fixed_vars[var_name] = model_or_intgs.variables.get(var_name) for key in fixed_vars.keys(): - if key not in self.fixed_vars: - self.fixed_vars[key] = fixed_vars.get(key) + if key not in self.model.variables: + raise ValueError(f'{key} is not a dynamical variable in {self.model}.') + self.fixed_vars = fixed_vars + + # check duplicate + for key in self.fixed_vars.keys(): + if key in self.target_vars: + raise errors.ModelUseError(f'"{key}" is defined as a target variable in "target_vars", ' + f'but also defined as a fixed variable in "fixed_vars".') # equations of dynamical variables # -------------------------------- - var2eq = {integrator.diff_eq.var_name: integrator for integrator in model_or_intgs.integrators} - target_func_args = set() + var2eq = {ana.var_name: ana for ana in self.model.analyzers} self.target_eqs = tools.DictPlus() for key in self.target_vars.keys(): if key not in var2eq: raise errors.ModelUseError(f'target "{key}" is not a dynamical variable.') - integrator = var2eq[key] - diff_eq = integrator.diff_eq + diff_eq = var2eq[key] sub_exprs = diff_eq.get_f_expressions(substitute_vars=list(self.target_vars.keys())) old_exprs = diff_eq.get_f_expressions(substitute_vars=None) self.target_eqs[key] = tools.DictPlus(sub_exprs=sub_exprs, old_exprs=old_exprs, diff_eq=diff_eq, func_name=diff_eq.func_name) - target_func_args.update(diff_eq.func_args) # parameters to update # --------------------- @@ -144,9 +143,8 @@ class BaseNeuronAnalyzer(object): raise errors.ModelUseError('"pars_update" must be a dict with the format ' 'of {"par1": val1, "par2": val2}.') for key in pars_update.keys(): - if key not in model_or_intgs.step_scopes: - if key not in target_func_args: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model.') + if (key not in self.model.scopes) and (key not in self.model.parameters): + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{self.model}" model.') self.pars_update = pars_update # dynamical parameters @@ -154,18 +152,22 @@ class BaseNeuronAnalyzer(object): if target_pars is None: target_pars = dict() if not isinstance(target_pars, dict): - raise errors.ModelUseError('"pars_dynamical" must be a dict with the format ' - 'of {"par1": (val1, val2)}.') + raise errors.ModelUseError('"target_pars" must be a dict with the format of {"par1": (val1, val2)}.') for key in target_pars.keys(): - if key not in model_or_intgs.step_scopes: - if key not in target_func_args: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model.') + if (key not in self.model.scopes) and (key not in self.model.parameters): + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{self.model}" model.') self.target_pars = target_pars if isinstance(self.target_vars, OrderedDict): self.dpar_names = list(self.target_pars.keys()) else: self.dpar_names = list(sorted(self.target_pars.keys())) + # check duplicate + for key in self.pars_update.keys(): + if key in self.target_pars: + raise errors.ModelUseError(f'"{key}" is defined as a target parameter in "target_pars", ' + f'but also defined as a fixed parameter in "pars_update".') + # resolutions for numerical methods # --------------------------------- self.resolutions = dict() @@ -244,7 +246,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): if 'dxdt' not in self.analyzed_results: scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integrators.get_mapping_scope()) + scope.update(sympy_analysis.get_mapping_scope()) scope.update(self.x_eq_group.diff_eq.func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) func_code = f'def func({argument}):\n' @@ -262,11 +264,11 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): x_var = self.dvar_names[0] x_symbol = sympy.Symbol(x_var, real=True) x_eq = self.x_eq_group.sub_exprs[-1].code - x_eq = integrators.str2sympy(x_eq) + x_eq = sympy_analysis.str2sympy(x_eq) eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integrators.get_mapping_scope()) + eq_x_scope.update(sympy_analysis.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -284,7 +286,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): # check all_vars = set(eq_x_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integrators.sympy2str(dfxdx_expr), all_vars): + if utils.contain_unknown_symbol(sympy_analysis.sympy2str(dfxdx_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -292,7 +294,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): func_codes = [f'def dfdx({argument}):'] for expr in self.x_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integrators.sympy2str(dfxdx_expr)}') + func_codes.append(f'return {sympy_analysis.sympy2str(dfxdx_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_x_scope) dfdx = eq_x_scope['dfdx'] sympy_failed = False @@ -322,13 +324,13 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): """ if 'fixed_point' not in self.analyzed_results: - x_eq = integrators.str2sympy(self.x_eq_group.sub_exprs[-1].code) + x_eq = sympy_analysis.str2sympy(self.x_eq_group.sub_exprs[-1].code) scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integrators.get_mapping_scope()) + scope.update(sympy_analysis.get_mapping_scope()) scope.update(self.x_eq_group.diff_eq.func_scope) - scope['numpy'] = np + scope['np'] = np timeout_len = self.options.sympy_solver_timeout argument1 = ', '.join(self.dvar_names + self.dpar_names) @@ -347,7 +349,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): for res in results: all_vars = set(scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integrators.sympy2str(res), all_vars): + if utils.contain_unknown_symbol(sympy_analysis.sympy2str(res), all_vars): print('failed because contain unknown symbols.') sympy_failed = True break @@ -357,7 +359,7 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): func_codes = [f'def solve_x({argument2}):'] for expr in self.x_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - result_expr = ', '.join([integrators.sympy2str(expr) for expr in results]) + result_expr = ', '.join([sympy_analysis.sympy2str(expr) for expr in results]) func_codes.append(f'_res_ = {result_expr}') func_codes.append(f'return np.array(_res_)') @@ -456,7 +458,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check "f" scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integrators.get_mapping_scope()) + scope.update(sympy_analysis.get_mapping_scope()) if a.endswith('y_eq'): scope.update(self.y_eq_group['diff_eq'].func_scope) else: @@ -487,7 +489,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_var = self.dvar_names[1] scope = deepcopy(self.pars_update) scope.update(self.fixed_vars) - scope.update(integrators.get_mapping_scope()) + scope.update(sympy_analysis.get_mapping_scope()) scope.update(self.y_eq_group.diff_eq.func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) func_code = f'def func({argument}):\n' @@ -505,11 +507,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_var = self.dvar_names[1] y_symbol = sympy.Symbol(y_var, real=True) x_eq = self.target_eqs[x_var].sub_exprs[-1].code - x_eq = integrators.str2sympy(x_eq) + x_eq = sympy_analysis.str2sympy(x_eq) eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integrators.get_mapping_scope()) + eq_x_scope.update(sympy_analysis.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -527,7 +529,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check all_vars = set(eq_x_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integrators.sympy2str(dfxdy_expr), all_vars): + if utils.contain_unknown_symbol(sympy_analysis.sympy2str(dfxdy_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -535,7 +537,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): func_codes = [f'def dfdy({argument}):'] for expr in self.x_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integrators.sympy2str(dfxdy_expr)}') + func_codes.append(f'return {sympy_analysis.sympy2str(dfxdy_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_x_scope) dfdy = eq_x_scope['dfdy'] sympy_failed = False @@ -568,11 +570,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): x_symbol = sympy.Symbol(x_var, real=True) y_var = self.dvar_names[1] y_eq = self.target_eqs[y_var].sub_exprs[-1].code - y_eq = integrators.str2sympy(y_eq) + y_eq = sympy_analysis.str2sympy(y_eq) eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integrators.get_mapping_scope()) + eq_y_scope.update(sympy_analysis.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -590,7 +592,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check all_vars = set(eq_y_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integrators.sympy2str(dfydx_expr), all_vars): + if utils.contain_unknown_symbol(sympy_analysis.sympy2str(dfydx_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -598,7 +600,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): func_codes = [f'def dgdx({argument}):'] for expr in self.y_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integrators.sympy2str(dfydx_expr)}') + func_codes.append(f'return {sympy_analysis.sympy2str(dfydx_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_y_scope) dgdx = eq_y_scope['dgdx'] sympy_failed = False @@ -631,11 +633,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_var = self.dvar_names[1] y_symbol = sympy.Symbol(y_var, real=True) y_eq = self.target_eqs[y_var].sub_exprs[-1].code - y_eq = integrators.str2sympy(y_eq) + y_eq = sympy_analysis.str2sympy(y_eq) eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integrators.get_mapping_scope()) + eq_y_scope.update(sympy_analysis.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -654,7 +656,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # check all_vars = set(eq_y_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) - if utils.contain_unknown_symbol(integrators.sympy2str(dfydx_expr), all_vars): + if utils.contain_unknown_symbol(sympy_analysis.sympy2str(dfydx_expr), all_vars): print('failed because contain unknown symbols.') sympy_failed = True else: @@ -662,7 +664,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): func_codes = [f'def dgdy({argument}):'] for expr in self.y_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - func_codes.append(f'return {integrators.sympy2str(dfydx_expr)}') + func_codes.append(f'return {sympy_analysis.sympy2str(dfydx_expr)}') exec(compile('\n '.join(func_codes), '', 'exec'), eq_y_scope) dgdy = eq_y_scope['dgdy'] sympy_failed = False @@ -714,12 +716,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): """ if 'fixed_point' not in self.analyzed_results: - vars_and_pars = ','.join(self.dvar_names[2:] + self.dpar_names) eq_xy_scope = deepcopy(self.pars_update) eq_xy_scope.update(self.fixed_vars) - eq_xy_scope.update(integrators.get_mapping_scope()) + eq_xy_scope.update(sympy_analysis.get_mapping_scope()) eq_xy_scope.update(self.x_eq_group['diff_eq'].func_scope) eq_xy_scope.update(self.y_eq_group['diff_eq'].func_scope) @@ -828,7 +829,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # f eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integrators.get_mapping_scope()) + eq_x_scope.update(sympy_analysis.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) func_codes = [f'def f_x({",".join(self.dvar_names + self.dpar_names)}):'] func_codes.extend([f'{expr.var_name} = {expr.code}' @@ -840,7 +841,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # g eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integrators.get_mapping_scope()) + eq_y_scope.update(sympy_analysis.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) func_codes = [f'def g_y({",".join(self.dvar_names + self.dpar_names)}):'] func_codes.extend([f'{expr.var_name} = {expr.code}' @@ -895,7 +896,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # x equation scope eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integrators.get_mapping_scope()) + eq_x_scope.update(sympy_analysis.get_mapping_scope()) eq_x_scope.update(self.x_eq_group.diff_eq.func_scope) argument = ','.join(self.dvar_names[2:] + self.dpar_names) @@ -969,7 +970,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): # y equation scope eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integrators.get_mapping_scope()) + eq_y_scope.update(sympy_analysis.get_mapping_scope()) eq_y_scope.update(self.y_eq_group.diff_eq.func_scope) argument = ','.join(self.dvar_names[2:] + self.dpar_names) @@ -1033,11 +1034,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: y_symbol = sympy.Symbol(self.y_var, real=True) code = self.target_eqs[self.y_var].sub_exprs[-1].code - y_eq = integrators.str2sympy(code).expr + y_eq = sympy_analysis.str2sympy(code).expr eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integrators.get_mapping_scope()) + eq_y_scope.update(sympy_analysis.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1053,7 +1054,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_by_x_in_y_eq = f() if len(y_by_x_in_y_eq) > 1: raise NotImplementedError('Do not support multiple values.') - y_by_x_in_y_eq = integrators.sympy2str(y_by_x_in_y_eq[0]) + y_by_x_in_y_eq = sympy_analysis.sympy2str(y_by_x_in_y_eq[0]) # check all_vars = set(eq_y_scope.keys()) @@ -1107,11 +1108,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: y_symbol = sympy.Symbol(self.y_var, real=True) code = self.x_eq_group.sub_exprs[-1].code - x_eq = integrators.str2sympy(code).expr + x_eq = sympy_analysis.str2sympy(code).expr eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integrators.get_mapping_scope()) + eq_x_scope.update(sympy_analysis.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1128,7 +1129,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): y_by_x_in_x_eq = f() if len(y_by_x_in_x_eq) > 1: raise NotImplementedError('Do not support multiple values.') - y_by_x_in_x_eq = integrators.sympy2str(y_by_x_in_x_eq[0]) + y_by_x_in_x_eq = sympy_analysis.sympy2str(y_by_x_in_x_eq[0]) all_vars = set(eq_x_scope.keys()) all_vars.update(self.dvar_names + self.dpar_names) @@ -1181,11 +1182,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: x_symbol = sympy.Symbol(self.x_var, real=True) code = self.target_eqs[self.y_var].sub_exprs[-1].code - y_eq = integrators.str2sympy(code).expr + y_eq = sympy_analysis.str2sympy(code).expr eq_y_scope = deepcopy(self.pars_update) eq_y_scope.update(self.fixed_vars) - eq_y_scope.update(integrators.get_mapping_scope()) + eq_y_scope.update(sympy_analysis.get_mapping_scope()) eq_y_scope.update(self.y_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1200,7 +1201,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): x_by_y_in_y_eq = f() if len(x_by_y_in_y_eq) > 1: raise NotImplementedError('Do not support multiple values.') - x_by_y_in_y_eq = integrators.sympy2str(x_by_y_in_y_eq[0]) + x_by_y_in_y_eq = sympy_analysis.sympy2str(x_by_y_in_y_eq[0]) # check all_vars = set(eq_y_scope.keys()) @@ -1254,11 +1255,11 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): if not self.options.escape_sympy_solver: x_symbol = sympy.Symbol(self.x_var, real=True) code = self.x_eq_group.sub_exprs[-1].code - x_eq = integrators.str2sympy(code).expr + x_eq = sympy_analysis.str2sympy(code).expr eq_x_scope = deepcopy(self.pars_update) eq_x_scope.update(self.fixed_vars) - eq_x_scope.update(integrators.get_mapping_scope()) + eq_x_scope.update(sympy_analysis.get_mapping_scope()) eq_x_scope.update(self.x_eq_group['diff_eq'].func_scope) argument = ', '.join(self.dvar_names + self.dpar_names) @@ -1273,7 +1274,7 @@ class Base2DNeuronAnalyzer(Base1DNeuronAnalyzer): x_by_y_in_x_eq = f() if len(x_by_y_in_x_eq) > 1: raise NotImplementedError('Do not support multiple values.') - x_by_y_in_x_eq = integrators.sympy2str(x_by_y_in_x_eq[0]) + x_by_y_in_x_eq = sympy_analysis.sympy2str(x_by_y_in_x_eq[0]) # check all_vars = set(eq_x_scope.keys()) diff --git a/brainpy/analysis/bifurcation.py b/brainpy/analysis/bifurcation.py index 8b003d22..9e90567f 100644 --- a/brainpy/analysis/bifurcation.py +++ b/brainpy/analysis/bifurcation.py @@ -7,10 +7,11 @@ import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.mplot3d import Axes3D -from brainpy import simulation +from brainpy import backend from brainpy import errors -from brainpy import profile +from brainpy import simulation from brainpy.analysis import base +from brainpy.analysis import dyn_model from brainpy.analysis import stability from brainpy.analysis import utils @@ -38,18 +39,16 @@ class Bifurcation(object): Parameters ---------- - model_or_intgs : NeuType - An abstract neuronal type defined in BrainPy. + integrals : callable + The integral functions defined with `brainpy.odeint` or + `brainpy.sdeint` or `brainpy.ddeint`, or `brainpy.fdeint`. """ - def __init__(self, model_or_intgs, target_pars, target_vars, fixed_vars=None, pars_update=None, + def __init__(self, integrals, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - # check "model" - if not isinstance(model_or_intgs, simulation.Population): - raise errors.ModelUseError('Bifurcation analysis only support neuron type model.') - self.model = model_or_intgs + self.model = dyn_model.transform_integrals_to_analyzers(integrals) # check "target_pars" if not isinstance(target_pars, dict): @@ -82,13 +81,13 @@ class Bifurcation(object): raise errors.ModelUseError('"pars_update" must be a dict the format of: ' '{"Par A": A_value, "Par B": B_value}') for key in pars_update.keys(): - if key not in model_or_intgs.step_scopes: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model. ') + if (key not in self.model.scopes) and (key not in self.model.parameters): + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{integrals}". ') self.pars_update = pars_update # bifurcation analysis if len(self.target_vars) == 1: - self.analyzer = _Bifurcation1D(model_or_intgs=model_or_intgs, + self.analyzer = _Bifurcation1D(model_or_integrals=self.model, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -97,7 +96,7 @@ class Bifurcation(object): options=options) elif len(self.target_vars) == 2: - self.analyzer = _Bifurcation2D(model_or_intgs=model_or_intgs, + self.analyzer = _Bifurcation2D(model_or_integrals=self.model, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -118,9 +117,9 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): Using this class, we can make co-dimension1 or co-dimension2 bifurcation analysis. """ - def __init__(self, model_or_intgs, target_pars, target_vars, fixed_vars=None, + def __init__(self, model_or_integrals, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_Bifurcation1D, self).__init__(model=model_or_intgs, + super(_Bifurcation1D, self).__init__(model_or_integrals=model_or_integrals, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -135,7 +134,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): f_dfdx = self.get_f_dfdx() if len(self.target_pars) == 1: - container = {c: {'p': [], 'x': []} for c in stability.get_1d_classification()} + container = {c: {'p': [], 'x': []} for c in stability.get_1d_stability_types()} # fixed point par_a = self.dpar_names[0] @@ -151,7 +150,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): plt.figure(self.x_var) for fp_type, points in container.items(): if len(points['x']): - plot_style = utils.plot_scheme[fp_type] + plot_style = stability.plot_scheme[fp_type] plt.plot(points['p'], points['x'], '.', **plot_style, label=fp_type) plt.xlabel(par_a) plt.ylabel(self.x_var) @@ -165,7 +164,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): plt.show() elif len(self.target_pars) == 2: - container = {c: {'p0': [], 'p1': [], 'x': []} for c in stability.get_1d_classification()} + container = {c: {'p0': [], 'p1': [], 'x': []} for c in stability.get_1d_stability_types()} # fixed point for p0 in self.resolutions[self.dpar_names[0]]: @@ -183,7 +182,7 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): ax = fig.gca(projection='3d') for fp_type, points in container.items(): if len(points['x']): - plot_style = utils.plot_scheme[fp_type] + plot_style = stability.plot_scheme[fp_type] xs = points['p0'] ys = points['p1'] zs = points['x'] @@ -212,16 +211,15 @@ class _Bifurcation1D(base.Base1DNeuronAnalyzer): raise NotImplementedError('1D phase plane do not support plot_limit_cycle_by_sim.') - class _Bifurcation2D(base.Base2DNeuronAnalyzer): """Bifurcation analysis of 2D system. Using this class, we can make co-dimension1 or co-dimension2 bifurcation analysis. """ - def __init__(self, model_or_intgs, target_pars, target_vars, fixed_vars=None, + def __init__(self, model_or_integrals, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_Bifurcation2D, self).__init__(model=model_or_intgs, + super(_Bifurcation2D, self).__init__(model_or_integrals=model_or_integrals, target_pars=target_pars, target_vars=target_vars, fixed_vars=fixed_vars, @@ -241,7 +239,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): # bifurcation analysis of co-dimension 1 if len(self.target_pars) == 1: container = {c: {'p': [], self.x_var: [], self.y_var: []} - for c in stability.get_2d_classification()} + for c in stability.get_2d_stability_types()} # fixed point for p in self.resolutions[self.dpar_names[0]]: @@ -258,7 +256,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): plt.figure(var) for fp_type, points in container.items(): if len(points['p']): - plot_style = utils.plot_scheme[fp_type] + plot_style = stability.plot_scheme[fp_type] plt.plot(points['p'], points[var], '.', **plot_style, label=fp_type) plt.xlabel(self.dpar_names[0]) plt.ylabel(var) @@ -274,7 +272,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): # bifurcation analysis of co-dimension 2 elif len(self.target_pars) == 2: container = {c: {'p0': [], 'p1': [], self.x_var: [], self.y_var: []} - for c in stability.get_2d_classification()} + for c in stability.get_2d_stability_types()} # fixed point for p0 in self.resolutions[self.dpar_names[0]]: @@ -294,7 +292,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): ax = fig.gca(projection='3d') for fp_type, points in container.items(): if len(points['p0']): - plot_style = utils.plot_scheme[fp_type] + plot_style = stability.plot_scheme[fp_type] xs = points['p0'] ys = points['p1'] zs = points[var] @@ -319,7 +317,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): self.fixed_points = container return container - + def plot_limit_cycle_by_sim(self, var, duration=100, inputs=(), plot_style=None, tol=0.001, show=False): print('plot limit cycle ...') @@ -465,12 +463,10 @@ class FastSlowBifurcation(object): """ - def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, + def __init__(self, integrals, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): # check "model" - if not isinstance(model_or_intgs, simulation.NeuType): - raise errors.ModelUseError('FastSlowBifurcation only support neuron type model.') - self.model = model_or_intgs + self.model = dyn_model.transform_integrals_to_analyzers(integrals) # check "fast_vars" if not isinstance(fast_vars, dict): @@ -507,13 +503,13 @@ class FastSlowBifurcation(object): raise errors.ModelUseError('"pars_update" must be a dict the format of: ' '{"Par A": A_value, "Par B": B_value}') for key in pars_update.keys(): - if key not in model_or_intgs.step_scopes: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model. ') + if (key not in self.model.scopes) or (key not in self.model.parameters): + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{integrals}" model. ') self.pars_update = pars_update # bifurcation analysis if len(self.fast_vars) == 1: - self.analyzer = _FastSlow1D(model_or_intgs=model_or_intgs, + self.analyzer = _FastSlow1D(model_or_integrals=self.model, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, @@ -522,7 +518,7 @@ class FastSlowBifurcation(object): options=options) elif len(self.fast_vars) == 2: - self.analyzer = _FastSlow2D(model_or_intgs=model_or_intgs, + self.analyzer = _FastSlow2D(model_or_integrals=self.model, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, @@ -546,7 +542,12 @@ class FastSlowBifurcation(object): class _FastSlowTrajectory(object): def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, **kwargs): - self.model = model_or_intgs + if isinstance(model_or_intgs, dyn_model.DynamicalModel): + self.model = model_or_intgs + elif (isinstance(model_or_intgs, (list, tuple)) and callable(model_or_intgs[0])) or callable(model_or_intgs): + self.model = dyn_model.transform_integrals_to_analyzers(model_or_intgs) + else: + raise ValueError self.fast_vars = fast_vars self.slow_vars = slow_vars self.fixed_vars = fixed_vars @@ -568,18 +569,19 @@ class _FastSlowTrajectory(object): else: self.slow_var_names = list(sorted(slow_vars.keys())) - # cannot update dynamical parameters - all_vars = self.fast_var_names + self.slow_var_names - self.traj_group = simulation.NeuGroup(model_or_intgs, - size=1, - monitors=all_vars, - pars_update=pars_update) - self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, - target_vars=all_vars, - fixed_vars=fixed_vars) - self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() - if not key.startswith('_')} - self.traj_net = simulation.Network(self.traj_group) + # TODO + # # cannot update dynamical parameters + # all_vars = self.fast_var_names + self.slow_var_names + # self.traj_group = simulation.NeuGroup(model_or_intgs, + # size=1, + # monitors=all_vars, + # pars_update=pars_update) + # self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, + # target_vars=all_vars, + # fixed_vars=fixed_vars) + # self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() + # if not key.startswith('_')} + # self.traj_net = simulation.Network(self.traj_group) def plot_trajectory(self, initials, duration, plot_duration=None, inputs=(), show=False): """Plot trajectories according to the settings. @@ -677,8 +679,8 @@ class _FastSlowTrajectory(object): legend = legend[:-2] # 5.4 trajectory - start = int(plot_duration[init_i][0] / profile.get_dt()) - end = int(plot_duration[init_i][1] / profile.get_dt()) + start = int(plot_duration[init_i][0] / backend.get_dt()) + end = int(plot_duration[init_i][1] / backend.get_dt()) # 5.5 visualization for var_name in self.fast_var_names: @@ -727,16 +729,16 @@ class _FastSlowTrajectory(object): class _FastSlow1D(_Bifurcation1D): - def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, + def __init__(self, model_or_integrals, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_FastSlow1D, self).__init__(model_or_intgs=model_or_intgs, + super(_FastSlow1D, self).__init__(model_or_integrals=model_or_integrals, target_pars=slow_vars, target_vars=fast_vars, fixed_vars=fixed_vars, pars_update=pars_update, numerical_resolution=numerical_resolution, options=options) - self.traj = _FastSlowTrajectory(model_or_intgs=model_or_intgs, + self.traj = _FastSlowTrajectory(model_or_intgs=model_or_integrals, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, @@ -755,16 +757,16 @@ class _FastSlow1D(_Bifurcation1D): class _FastSlow2D(_Bifurcation2D): - def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, + def __init__(self, model_or_integrals, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): - super(_FastSlow2D, self).__init__(model_or_intgs=model_or_intgs, + super(_FastSlow2D, self).__init__(model_or_integrals=model_or_integrals, target_pars=slow_vars, target_vars=fast_vars, fixed_vars=fixed_vars, pars_update=pars_update, numerical_resolution=numerical_resolution, options=options) - self.traj = _FastSlowTrajectory(model_or_intgs=model_or_intgs, + self.traj = _FastSlowTrajectory(model_or_intgs=model_or_integrals, fast_vars=fast_vars, slow_vars=slow_vars, fixed_vars=fixed_vars, diff --git a/brainpy/analysis/dyn_model.py b/brainpy/analysis/dyn_model.py new file mode 100644 index 00000000..84819bd3 --- /dev/null +++ b/brainpy/analysis/dyn_model.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + + +import inspect + +from brainpy.integrators import ast_analysis +from brainpy.integrators import sympy_analysis + +try: + from numba.core.dispatcher import Dispatcher +except ModuleNotFoundError: + Dispatcher = None + +__all__ = [ + 'transform_integrals_to_analyzers', + 'DynamicalModel', +] + + +def transform_integrals_to_analyzers(integrals): + if callable(integrals): + integrals = [integrals] + + all_scope = dict() + all_variables = set() + all_parameters = set() + analyzers = [] + for integral in integrals: + # integral function + if Dispatcher is not None and isinstance(integral, Dispatcher): + integral = integral.py_func + else: + integral = integral + + + + # original function + f = integral.origin_f + if Dispatcher is not None and isinstance(f, Dispatcher): + f = f.py_func + func_name = f.__name__ + + # code scope + closure_vars = inspect.getclosurevars(f) + code_scope = dict(closure_vars.nonlocals) + code_scope.update(dict(closure_vars.globals)) + + # separate variables + analysis = ast_analysis.separate_variables(f) + variables_for_returns = analysis['variables_for_returns'] + expressions_for_returns = analysis['expressions_for_returns'] + for vi, (key, vars) in enumerate(variables_for_returns.items()): + variables = [] + for v in vars: + if len(v) > 1: + raise ValueError('Cannot analyze must assignment code line.') + variables.append(v[0]) + expressions = expressions_for_returns[key] + var_name = integral.variables[vi] + DE = sympy_analysis.SingleDiffEq(var_name=var_name, + variables=variables, + expressions=expressions, + derivative_expr=key, + scope=code_scope, + func_name=func_name) + analyzers.append(DE) + + # others + all_variables.update(integral.variables) + all_parameters.update(integral.parameters) + all_scope.update(code_scope) + + return DynamicalModel(analyzers=analyzers, + variables=list(all_variables), + parameters=list(all_parameters), + scopes=all_scope) + + +class DynamicalModel(object): + def __init__(self, analyzers, variables, parameters, scopes): + self.analyzers = analyzers + self.variables = variables + self.parameters = parameters + self.scopes = scopes diff --git a/brainpy/analysis/phase_plane.py b/brainpy/analysis/phase_plane.py index 4f276a94..4b5b4973 100644 --- a/brainpy/analysis/phase_plane.py +++ b/brainpy/analysis/phase_plane.py @@ -3,10 +3,12 @@ import matplotlib.pyplot as plt import numpy as np -from brainpy import simulation +from brainpy import backend from brainpy import errors -from brainpy import profile +from brainpy import simulation from brainpy.analysis import base +from brainpy.analysis import dyn_model +from brainpy.analysis import stability from brainpy.analysis import utils __all__ = [ @@ -26,7 +28,7 @@ class PhasePlane(object): Parameters ---------- - model_or_intgs : NeuType + integrals : NeuType The neuron model which defines the differential equations by using `brainpy.integrate`. target_vars : dict @@ -76,23 +78,20 @@ class PhasePlane(object): def __init__( self, - model_or_intgs, + integrals, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None, ): - # check "model" - if not isinstance(model_or_intgs, simulation.Population): - raise errors.ModelUseError('Phase plane analysis only support neuron type model.') - self.model = model_or_intgs + self.model = dyn_model.transform_integrals_to_analyzers(integrals) # check "target_vars" if not isinstance(target_vars, dict): raise errors.ModelUseError('"target_vars" must a dict with the format of: ' - '{"Variable A": [A_min, A_max], "Variable B": [B_min, B_max]}') + '{"Variable A": [A_min, A_max], "Variable B": [B_min, B_max]}') self.target_vars = target_vars # check "fixed_vars" @@ -100,7 +99,7 @@ class PhasePlane(object): fixed_vars = dict() if not isinstance(fixed_vars, dict): raise errors.ModelUseError('"fixed_vars" must be a dict with the format of: ' - '{"Variable A": A_value, "Variable B": B_value}') + '{"Variable A": A_value, "Variable B": B_value}') self.fixed_vars = fixed_vars # check "pars_update" @@ -108,22 +107,22 @@ class PhasePlane(object): pars_update = dict() if not isinstance(pars_update, dict): raise errors.ModelUseError('"pars_update" must be a dict with the format of: ' - '{"Par A": A_value, "Par B": B_value}') + '{"Par A": A_value, "Par B": B_value}') for key in pars_update.keys(): - if key not in model_or_intgs.step_scopes: - raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{model_or_intgs.name}" model.') + if (key not in self.model.scopes) and (key not in self.model.parameters): + raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{integrals}" model.') self.pars_update = pars_update # analyzer if len(target_vars) == 1: - self.analyzer = _PhasePlane1D(model=model_or_intgs, + self.analyzer = _PhasePlane1D(model_or_integrals=self.model, target_vars=target_vars, fixed_vars=fixed_vars, pars_update=pars_update, numerical_resolution=numerical_resolution, options=options) elif len(target_vars) == 2: - self.analyzer = _PhasePlane2D(model=model_or_intgs, + self.analyzer = _PhasePlane2D(model_or_integrals=self.model, target_vars=target_vars, fixed_vars=fixed_vars, pars_update=pars_update, @@ -131,8 +130,8 @@ class PhasePlane(object): options=options) else: raise errors.ModelUseError('BrainPy only support 1D/2D phase plane analysis. ' - 'Or, you can set "fixed_vars" to fix other variables, ' - 'then make 1D/2D phase plane analysis.') + 'Or, you can set "fixed_vars" to fix other variables, ' + 'then make 1D/2D phase plane analysis.') def plot_vector_field(self, *args, **kwargs): """Plot vector filed of a 2D/1D system.""" @@ -182,7 +181,7 @@ class _PhasePlane1D(base.Base1DNeuronAnalyzer): y_val = self.get_f_dx()(self.resolutions[self.x_var]) except TypeError: raise errors.ModelUseError('Missing variables. Please check and set missing ' - 'variables to "fixed_vars".') + 'variables to "fixed_vars".') # 2. visualization label = f"d{self.x_var}dt" @@ -192,7 +191,8 @@ class _PhasePlane1D(base.Base1DNeuronAnalyzer): plt.xlabel(self.x_var) plt.ylabel(label) - plt.xlim(*utils.rescale(self.target_vars[self.x_var], scale=(self.options.lim_scale - 1.) / 2)) + plt.xlim(*utils.rescale(self.target_vars[self.x_var], + scale=(self.options.lim_scale - 1.) / 2)) plt.legend() if show: plt.show() @@ -219,18 +219,18 @@ class _PhasePlane1D(base.Base1DNeuronAnalyzer): # 2. stability analysis x_values = f_fixed_point() - container = {a: [] for a in utils.get_1d_classification()} + container = {a: [] for a in stability.get_1d_stability_types()} for i in range(len(x_values)): x = x_values[i] dfdx = f_dfdx(x) - fp_type = utils.stability_analysis(dfdx) + fp_type = stability.stability_analysis(dfdx) print(f"Fixed point #{i + 1} at {self.x_var}={x} is a {fp_type}.") container[fp_type].append(x) # 3. visualization for fp_type, points in container.items(): if len(points): - plot_style = utils.plot_scheme[fp_type] + plot_style = stability.plot_scheme[fp_type] plt.plot(points, [0] * len(points), '.', markersize=20, **plot_style, label=fp_type) plt.legend() @@ -251,26 +251,27 @@ class _PhasePlane1D(base.Base1DNeuronAnalyzer): class _PhasePlane2D(base.Base2DNeuronAnalyzer): """Phase plane analyzer for 2D system. - """ def __init__(self, *args, **kwargs): super(_PhasePlane2D, self).__init__(*args, **kwargs) - # runner for trajectory - # --------------------- - - # cannot update dynamical parameters - self.traj_group = simulation.NeuGroup(self.model, - size=1, - monitors=self.dvar_names, - pars_update=self.pars_update) - self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, - target_vars=self.dvar_names, - fixed_vars=self.fixed_vars) - self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() - if not key.startswith('_')} - self.traj_net = simulation.Network(self.traj_group) + + # TODO + # # runner for trajectory + # # --------------------- + # + # # cannot update dynamical parameters + # self.traj_group = simulation.NeuGroup(self.model, + # size=1, + # monitors=self.dvar_names, + # pars_update=self.pars_update) + # self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, + # target_vars=self.dvar_names, + # fixed_vars=self.fixed_vars) + # self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() + # if not key.startswith('_')} + # self.traj_net = simulation.Network(self.traj_group) def plot_vector_field(self, plot_method='streamplot', plot_style=None, show=False): """Plot the vector field. @@ -309,14 +310,14 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): dx = self.get_f_dx()(X, Y) except TypeError: raise errors.ModelUseError('Missing variables. Please check and set missing ' - 'variables to "fixed_vars".') + 'variables to "fixed_vars".') # dy try: dy = self.get_f_dy()(X, Y) except TypeError: raise errors.ModelUseError('Missing variables. Please check and set missing ' - 'variables to "fixed_vars".') + 'variables to "fixed_vars".') # vector field if plot_method == 'quiver': @@ -374,11 +375,11 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): # stability analysis # ------------------ - container = {a: {'x': [], 'y': []} for a in utils.get_2d_classification()} + container = {a: {'x': [], 'y': []} for a in stability.get_2d_stability_types()} for i in range(len(x_values)): x = x_values[i] y = y_values[i] - fp_type = utils.stability_analysis(f_jacobian(x, y)) + fp_type = stability.stability_analysis(f_jacobian(x, y)) print(f"Fixed point #{i + 1} at {self.x_var}={x}, {self.y_var}={y} is a {fp_type}.") container[fp_type]['x'].append(x) container[fp_type]['y'].append(y) @@ -387,7 +388,7 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): # ------------- for fp_type, points in container.items(): if len(points['x']): - plot_style = utils.plot_scheme[fp_type] + plot_style = stability.plot_scheme[fp_type] plt.plot(points['x'], points['y'], '.', markersize=20, **plot_style, label=fp_type) plt.legend() if show: @@ -442,7 +443,7 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): y_values_in_y_eq = y_by_x['f'](xs) except TypeError: raise errors.ModelUseError('Missing variables. Please check and set missing ' - 'variables to "fixed_vars".') + 'variables to "fixed_vars".') x_values_in_y_eq = xs plt.plot(xs, y_values_in_y_eq, **y_style, label=f"{self.y_var} nullcline") @@ -453,7 +454,7 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): x_values_in_y_eq = x_by_y['f'](ys) except TypeError: raise errors.ModelUseError('Missing variables. Please check and set missing ' - 'variables to "fixed_vars".') + 'variables to "fixed_vars".') y_values_in_y_eq = ys plt.plot(x_values_in_y_eq, ys, **y_style, label=f"{self.y_var} nullcline") else: @@ -476,7 +477,7 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): y_values_in_x_eq = y_by_x['f'](xs) except TypeError: raise errors.ModelUseError('Missing variables. Please check and set missing ' - 'variables to "fixed_vars".') + 'variables to "fixed_vars".') x_values_in_x_eq = xs plt.plot(xs, y_values_in_x_eq, **x_style, label=f"{self.x_var} nullcline") @@ -487,7 +488,7 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): x_values_in_x_eq = x_by_y['f'](ys) except TypeError: raise errors.ModelUseError('Missing variables. Please check and set missing ' - 'variables to "fixed_vars".') + 'variables to "fixed_vars".') y_values_in_x_eq = ys plt.plot(x_values_in_x_eq, ys, **x_style, label=f"{self.x_var} nullcline") else: @@ -588,9 +589,10 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): # 4. format the inputs if len(inputs): if isinstance(inputs[0], (tuple, list)): - inputs = [(self.traj_group, ) + tuple(input) for input in inputs] + inputs = [(self.traj_group,) + tuple(input) + for input in inputs] elif isinstance(inputs[0], str): - inputs = [(self.traj_group, ) + tuple(inputs)] + inputs = [(self.traj_group,) + tuple(inputs)] else: raise errors.ModelUseError() @@ -616,14 +618,14 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): legend = legend[:-2] # 5.4 trajectory - start = int(plot_duration[init_i][0] / profile.get_dt()) - end = int(plot_duration[init_i][1] / profile.get_dt()) + start = int(plot_duration[init_i][0] / backend.get_dt()) + end = int(plot_duration[init_i][1] / backend.get_dt()) # 5.5 visualization if axes == 'v-v': lines = plt.plot(self.traj_group.mon[self.x_var][start: end, 0], - self.traj_group.mon[self.y_var][start: end, 0], - label=legend) + self.traj_group.mon[self.y_var][start: end, 0], + label=legend) utils.add_arrow(lines[0]) else: plt.plot(self.traj_group.mon.ts[start: end], @@ -697,9 +699,9 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): # 4. format the inputs if len(inputs): if isinstance(inputs[0], (tuple, list)): - inputs = [(self.traj_group, ) + tuple(input) for input in inputs] + inputs = [(self.traj_group,) + tuple(input) for input in inputs] elif isinstance(inputs[0], str): - inputs = [(self.traj_group, ) + tuple(inputs)] + inputs = [(self.traj_group,) + tuple(inputs)] else: raise errors.ModelUseError() @@ -739,4 +741,3 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): if show: plt.show() - diff --git a/brainpy/analysis/stability.py b/brainpy/analysis/stability.py index ae9eb663..bf338ed3 100644 --- a/brainpy/analysis/stability.py +++ b/brainpy/analysis/stability.py @@ -2,7 +2,6 @@ import numpy as np - CENTER_MANIFOLD = 'center manifold' SADDLE_NODE = 'saddle node' STABLE_POINT_1D = 'stable point' @@ -18,28 +17,52 @@ UNSTABLE_STAR_2D = 'unstable star' UNSTABLE_DEGENERATE_2D = 'unstable degenerate' UNSTABLE_LINE_2D = 'unstable line' +plot_scheme = { + STABLE_POINT_1D: {"color": 'tab:red'}, + STABLE_NODE_2D: {"color": 'tab:red'}, + + UNSTABLE_POINT_1D: {"color": 'tab:olive'}, + UNSTABLE_NODE_2D: {"color": 'tab:olive'}, + + STABLE_FOCUS_2D: {"color": 'tab:purple'}, + UNSTABLE_FOCUS_2D: {"color": 'tab:cyan'}, + + SADDLE_NODE: {"color": 'tab:blue'}, + CENTER_2D: {'color': 'lime'}, + # _2D_UNIFORM_MOTION: {'color': 'red'}, + + CENTER_MANIFOLD: {'color': 'orangered'}, + UNSTABLE_LINE_2D: {'color': 'dodgerblue'}, + + UNSTABLE_STAR_2D: {'color': 'green'}, + STABLE_STAR_2D: {'color': 'orange'}, + + UNSTABLE_DEGENERATE_2D: {'color': 'springgreen'}, + STABLE_DEGENERATE_2D: {'color': 'blueviolet'}, +} + -def get_1d_classification(): +def get_1d_stability_types(): return [SADDLE_NODE, STABLE_POINT_1D, UNSTABLE_POINT_1D] -def get_2d_classification(): +def get_2d_stability_types(): return [SADDLE_NODE, CENTER_2D, STABLE_NODE_2D, STABLE_FOCUS_2D, STABLE_STAR_2D, CENTER_MANIFOLD, UNSTABLE_NODE_2D, UNSTABLE_FOCUS_2D, UNSTABLE_STAR_2D, UNSTABLE_LINE_2D, STABLE_DEGENERATE_2D, UNSTABLE_DEGENERATE_2D] -def get_3d_classification(): +def get_3d_stability_types(): return [] -def stability_analysis(derivative): +def stability_analysis(derivatives): """Stability analysis for fixed point [1]_. Parameters ---------- - derivative : float, tuple, list, np.ndarray + derivatives : float, tuple, list, np.ndarray The derivative of the f. Returns @@ -53,19 +76,19 @@ def stability_analysis(derivative): .. [1] http://www.egwald.ca/nonlineardynamics/twodimensionaldynamics.php """ - if np.size(derivative) == 1: - if derivative == 0: + if np.size(derivatives) == 1: + if derivatives == 0: return SADDLE_NODE - elif derivative > 0: + elif derivatives > 0: return STABLE_POINT_1D else: return UNSTABLE_POINT_1D - elif np.size(derivative) == 4: - a = derivative[0][0] - b = derivative[0][1] - c = derivative[1][0] - d = derivative[1][1] + elif np.size(derivatives) == 4: + a = derivatives[0][0] + b = derivatives[0][1] + c = derivatives[1][0] + d = derivatives[1][1] # trace p = a + d @@ -91,7 +114,7 @@ def stability_analysis(derivative): elif e > 0: return UNSTABLE_NODE_2D else: - w = np.linalg.eigvals(derivative) + w = np.linalg.eigvals(derivatives) if w[0] == w[1]: return UNSTABLE_DEGENERATE_2D else: @@ -102,13 +125,13 @@ def stability_analysis(derivative): elif e > 0: return STABLE_NODE_2D else: - w = np.linalg.eigvals(derivative) + w = np.linalg.eigvals(derivatives) if w[0] == w[1]: return STABLE_DEGENERATE_2D else: return STABLE_STAR_2D - elif np.size(derivative) == 9: + elif np.size(derivatives) == 9: pass else: diff --git a/brainpy/analysis/utils.py b/brainpy/analysis/utils.py index ed6798ef..9b77bc4a 100644 --- a/brainpy/analysis/utils.py +++ b/brainpy/analysis/utils.py @@ -6,9 +6,9 @@ import threading import numpy as np +from brainpy.integrators import sympy_analysis from brainpy import backend from brainpy import tools -from . import stability try: import numba @@ -17,7 +17,6 @@ except ModuleNotFoundError: numba = None Dispatcher = None - __all__ = [ 'rescale', 'timeout', @@ -26,33 +25,6 @@ __all__ = [ 'contain_unknown_symbol', ] -plot_scheme = { - stability.STABLE_POINT_1D: {"color": 'tab:red'}, - stability.STABLE_NODE_2D: {"color": 'tab:red'}, - - stability.UNSTABLE_POINT_1D: {"color": 'tab:olive'}, - stability.UNSTABLE_NODE_2D: {"color": 'tab:olive'}, - - stability.STABLE_FOCUS_2D: {"color": 'tab:purple'}, - stability.UNSTABLE_FOCUS_2D: {"color": 'tab:cyan'}, - - stability.SADDLE_NODE: {"color": 'tab:blue'}, - stability.CENTER_2D: {'color': 'lime'}, - # stability._2D_UNIFORM_MOTION: {'color': 'red'}, - - stability.CENTER_MANIFOLD: {'color': 'orangered'}, - stability.UNSTABLE_LINE_2D: {'color': 'dodgerblue'}, - - stability.UNSTABLE_STAR_2D: {'color': 'green'}, - stability.STABLE_STAR_2D: {'color': 'orange'}, - - stability.UNSTABLE_DEGENERATE_2D: {'color': 'springgreen'}, - stability.STABLE_DEGENERATE_2D: {'color': 'blueviolet'}, -} - - -def get_integrators(population): - pass def rescale(min_max, scale=0.01): @@ -94,7 +66,7 @@ def timeout(s): def _jit(func): - if backend.func_in_numpy_or_math(func): + if sympy_analysis.func_in_numpy_or_math(func): return func if isinstance(func, Dispatcher): return func @@ -107,7 +79,7 @@ def _jit(func): for k, v in code_scope.items(): # function if callable(v): - if (not backend.func_in_numpy_or_math(v)) and (not isinstance(v, Dispatcher)): + if (not sympy_analysis.func_in_numpy_or_math(v)) and (not isinstance(v, Dispatcher)): code_scope[k] = _jit(v) modified = True @@ -127,7 +99,7 @@ def jit_compile(scope, func_code, func_name): func_scope = dict() for key, val in scope.items(): if callable(val): - if backend.func_in_numpy_or_math(val): + if sympy_analysis.func_in_numpy_or_math(val): pass elif isinstance(val, Dispatcher): pass diff --git a/brainpy/backend/__init__.py b/brainpy/backend/__init__.py index 10257270..f145d50f 100644 --- a/brainpy/backend/__init__.py +++ b/brainpy/backend/__init__.py @@ -8,14 +8,22 @@ from .operators.bk_numpy import * _backend = 'numpy' # default backend is NumPy _node_runner = None _net_runner = None -NEEDED_OPS = ['normal', 'reshape', 'shape', 'exp', 'sum', 'zeros', +_dt = 0.1 + +CLASS_KEYWORDS = ['self', 'cls'] +NEEDED_OPS = ['as_tensor', 'normal', 'reshape', 'shape', + 'exp', 'sum', 'zeros', 'ones', 'eye', 'matmul', 'vstack', 'arange'] SUPPORTED_BACKEND = ['numba', 'numba-parallel', 'numba-cuda', 'jax', 'numpy', 'pytorch', 'tensorflow', ] SYSTEM_KEYWORDS = ['_dt', '_t', '_i'] -def set(backend, module_or_operations=None, node_runner=None, net_runner=None): +def set(backend, module_or_operations=None, node_runner=None, + net_runner=None, dt=None): + if dt is not None: + set_dt(dt) + if _backend == backend: return @@ -94,6 +102,36 @@ def set(backend, module_or_operations=None, node_runner=None, net_runner=None): 'or a dict of operations.') + +def set_class_keywords(*args): + global CLASS_KEYWORDS + CLASS_KEYWORDS = list(args) + + +def set_dt(dt): + """Set the numerical integrator precision. + + Parameters + ---------- + dt : float + Numerical integration precision. + """ + assert isinstance(dt, float) + global _dt + _dt = dt + + +def get_dt(): + """Get the numerical integrator precision. + + Returns + ------- + dt : float + Numerical integration precision. + """ + return _dt + + def set_ops_from_module(module): global_vars = globals() for ops in NEEDED_OPS: diff --git a/brainpy/backend/operators/bk_numba_cpu.py b/brainpy/backend/operators/bk_numba_cpu.py index 425fe0c7..f6b36f2a 100644 --- a/brainpy/backend/operators/bk_numba_cpu.py +++ b/brainpy/backend/operators/bk_numba_cpu.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -import numba import numpy as np from . import bk_numba_overload - as_tensor = np.asarray normal = np.random.normal reshape = np.reshape @@ -19,23 +17,8 @@ matmul = np.matmul vstack = np.vstack arange = np.arange shape = np.shape +where = np.where -# -# @numba.njit -# def shape(x): -# size = np.shape(x) -# if len(size) == 0: -# return (1,) -# else: -# return size - - -@numba.generated_jit(fastmath=True, nopython=True, nogil=True) -def normal_like(x): - if isinstance(x, (numba.types.Integer, numba.types.Float)): - return lambda x: np.random.normal() - else: - return lambda x: np.random.normal(0., 1.0, x.shape) if __name__ == '__main__': diff --git a/brainpy/backend/operators/bk_numba_cuda.py b/brainpy/backend/operators/bk_numba_cuda.py index bef7c0fa..633f8661 100644 --- a/brainpy/backend/operators/bk_numba_cuda.py +++ b/brainpy/backend/operators/bk_numba_cuda.py @@ -1,4 +1,2 @@ # -*- coding: utf-8 -*- -from numba import cuda - diff --git a/brainpy/backend/operators/bk_numpy.py b/brainpy/backend/operators/bk_numpy.py index b71e38d6..538d1924 100644 --- a/brainpy/backend/operators/bk_numpy.py +++ b/brainpy/backend/operators/bk_numpy.py @@ -16,8 +16,10 @@ __all__ = [ 'vstack', 'arange', 'moveaxis', + 'where', ] + as_tensor = np.asarray normal = np.random.normal reshape = np.reshape @@ -30,6 +32,7 @@ matmul = np.matmul vstack = np.vstack arange = np.arange moveaxis = np.moveaxis +where = np.where def shape(x): diff --git a/brainpy/backend/operators/bk_pytorch.py b/brainpy/backend/operators/bk_pytorch.py index 1a0528c0..7fca4e21 100644 --- a/brainpy/backend/operators/bk_pytorch.py +++ b/brainpy/backend/operators/bk_pytorch.py @@ -15,8 +15,7 @@ sum = torch.sum zeros = torch.zeros ones = torch.ones eye = torch.eye -outer = torch.outer -dot = torch.mm +matmul = torch.matmul vstack = torch.vstack arange = torch.arange diff --git a/brainpy/backend/operators/standard.py b/brainpy/backend/operators/standard.py index e7e5574a..db364338 100644 --- a/brainpy/backend/operators/standard.py +++ b/brainpy/backend/operators/standard.py @@ -8,14 +8,9 @@ functions for computation backends. import numpy as np +def sum(tensor, axis=None): + """The sum operation. We expect "sum" function will behave like "numpy.sum" -def sum(tensor, axis): - """The sum operation. - - We expect "sum" function will behave like "numpy.sum" - - - Parameters ---------- tensor : array_like @@ -62,5 +57,3 @@ def sum(tensor, axis): pass - - diff --git a/brainpy/backend/runners/general_runner.py b/brainpy/backend/runners/general_runner.py index 6b9cb6bf..7cfe15ad 100644 --- a/brainpy/backend/runners/general_runner.py +++ b/brainpy/backend/runners/general_runner.py @@ -123,8 +123,7 @@ class GeneralNodeRunner(runner.NodeRunner): } def get_steps_func(self, show_code=False): - for step in self.steps: - func_name = step.__name__ + for func_name, step in self.steps.items(): class_args, arguments = utils.get_args(step) host_name = self.host.name @@ -230,12 +229,15 @@ class GeneralNetRunner(runner.NetRunner): code_scope = {} code_lines = ['def run_func(_t, _i, _dt):'] for obj in self.all_nodes.values(): - f, codes = obj.build(formatted_inputs=formatted_inputs.get(obj.name, []), + f, codes = obj.build(inputs=formatted_inputs.get(obj.name, []), + input_is_formatted=True, mon_length=run_length, return_code=True, show_code=show_code) need_rebuild *= codes['need_rebuild'] for p in obj.get_schedule(): + if (p not in codes) and (p in ['input', 'monitor']): + continue p_codes = codes[p] code_scope.update(p_codes['scope']) code_lines.extend(p_codes['call']) diff --git a/brainpy/backend/runners/jax_runner.py b/brainpy/backend/runners/jax_runner.py index 3511c7f6..7b5b75a7 100644 --- a/brainpy/backend/runners/jax_runner.py +++ b/brainpy/backend/runners/jax_runner.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -class JaxRunner(): +class JaxRunner(object): pass diff --git a/brainpy/backend/runners/numba_cpu_runner.py b/brainpy/backend/runners/numba_cpu_runner.py index 95d515e6..0e1b6788 100644 --- a/brainpy/backend/runners/numba_cpu_runner.py +++ b/brainpy/backend/runners/numba_cpu_runner.py @@ -5,11 +5,12 @@ import inspect import re import numba +from numba.core.dispatcher import Dispatcher from brainpy import backend from brainpy import errors -from brainpy import profile from brainpy import tools +from brainpy.simulation import delay from . import utils from .general_runner import GeneralNodeRunner @@ -66,32 +67,36 @@ def get_numba_profile(): class StepFuncReader(ast.NodeVisitor): - def __init__(self): + def __init__(self, host): self.lefts = [] self.rights = [] self.lines = [] + self.host = host + # get delay information + self.delay_call = {} + def visit_Assign(self, node, level=0): + self.generic_visit(node) + prefix = ' ' * level + expr = tools.ast2code(ast.fix_missing_locations(node.value)) + self.rights.append(expr) targets = [] for target in node.targets: targets.append(tools.ast2code(ast.fix_missing_locations(target))) - target = ' = '.join(targets) - self.lefts.append(target) - expr = tools.ast2code(ast.fix_missing_locations(node.value)) - self.rights.append(expr) - prefix = ' ' * level - self.lines.append(f'{prefix}{target} = {expr}') - return node + _target = ' = '.join(targets) + self.lefts.append(_target) + self.lines.append(f'{prefix}{_target} = {expr}') def visit_AugAssign(self, node, level=0): - target = tools.ast2code(ast.fix_missing_locations(node.target)) + self.generic_visit(node) + prefix = ' ' * level op = tools.ast2code(ast.fix_missing_locations(node.op)) expr = tools.ast2code(ast.fix_missing_locations(node.value)) - prefix = ' ' * level + target = tools.ast2code(ast.fix_missing_locations(node.target)) self.lefts.append(target) - self.rights.append(f"{target} {op} {expr}") - self.lines.append(f"{prefix}{target} {op}= {expr}") - return node + self.rights.append(f'{target} {op} {expr}') + self.lines.append(f"{prefix}{target} = {target} {op} {expr}") def visit_AnnAssign(self, node): raise NotImplementedError('Do not support an assignment with ' @@ -101,6 +106,9 @@ class StepFuncReader(ast.NodeVisitor): prefix = ' ' * level expr = tools.ast2code(ast.fix_missing_locations(node)) self.lines.append(f'{prefix}{expr}') + self.lefts.append('') + self.rights.append(expr) + self.generic_visit(node) def visit_Assert(self, node, level=0): self.visit_node_not_assign(node, level) @@ -126,15 +134,73 @@ class StepFuncReader(ast.NodeVisitor): self.visit_For(node, level) elif isinstance(node, ast.While): self.visit_While(node, level) + elif isinstance(node, ast.Call): + self.visit_Call(node, level) + elif isinstance(node, ast.Raise): + self.visit_Raise(node, level) else: code = tools.ast2code(ast.fix_missing_locations(node)) raise errors.CodeError(f'BrainPy does not support {type(node)} ' f'in Numba backend.\n\n{code}') + def visit_attr(self, node): + if isinstance(node, ast.Attribute): + r = self.visit_attr(node.value) + return [node.attr] + r + elif isinstance(node, ast.Name): + return [node.id] + else: + raise ValueError + + def visit_Call(self, node, level=0): + if node in self.delay_call: + return + calls = self.visit_attr(node.func) + calls = calls[::-1] + + # delay push / delay pull + if calls[-1] in ['push', 'pull']: + obj = self.host + for data in calls[1:-1]: + obj = getattr(obj, data) + obj_func = getattr(obj, calls[-1]) + if isinstance(obj, delay.ConstantDelay) and callable(obj_func): + func = ".".join(calls) + args = [] + for arg in node.args: + args.append(tools.ast2code(ast.fix_missing_locations(arg))) + keywords = [] + for arg in node.keywords: + keywords.append(tools.ast2code(ast.fix_missing_locations(arg))) + delay_var = '.'.join([self.host.name] + calls[1:-1]) + if calls[-1] == 'push': + kws_append = [f'delay_data={delay_var}_delay_data', + f'delay_in_idx={delay_var}_delay_in_idx', ] + data_need_pass = [f'{self.host.name}.{".".join(calls[1:-1])}.delay_data', + f'{self.host.name}.{".".join(calls[1:-1])}.delay_in_idx'] + else: + kws_append = [f'delay_data={delay_var}_delay_data', + f'delay_out_idx={delay_var}_delay_out_idx', ] + data_need_pass = [f'{self.host.name}.{".".join(calls[1:-1])}.delay_data', + f'{self.host.name}.{".".join(calls[1:-1])}.delay_out_idx'] + org_call = tools.ast2code(ast.fix_missing_locations(node)) + rep_call = f'{func}({", ".join(args + keywords + kws_append)})' + self.delay_call[node] = dict(type=calls[-1], + args=args, + keywords=keywords, + kws_append=kws_append, + func=func, + org_call=org_call, + rep_call=rep_call, + data_need_pass=data_need_pass) + + self.generic_visit(node) + def visit_If(self, node, level=0): # If condition prefix = ' ' * level compare = tools.ast2code(ast.fix_missing_locations(node.test)) + self.rights.append(f'if {compare}:') self.lines.append(f'{prefix}if {compare}:') # body for expr in node.body: @@ -153,6 +219,7 @@ class StepFuncReader(ast.NodeVisitor): self.lines.append(f'{prefix}else:') for expr in node.orelse: self.visit_content_in_condition_control(expr, level + 1) + self.generic_visit(node) def visit_For(self, node, level=0): prefix = ' ' * level @@ -160,8 +227,7 @@ class StepFuncReader(ast.NodeVisitor): target = tools.ast2code(ast.fix_missing_locations(node.target)) # iter iter = tools.ast2code(ast.fix_missing_locations(node.iter)) - self.lefts.append(target) - self.rights.append(iter) + self.rights.append(f'{target} in {iter}') self.lines.append(prefix + f'for {target} in {iter}:') # body for expr in node.body: @@ -171,6 +237,7 @@ class StepFuncReader(ast.NodeVisitor): self.lines.append(prefix + 'else:') for expr in node.orelse: self.visit_content_in_condition_control(expr, level + 1) + self.generic_visit(node) def visit_While(self, node, level=0): prefix = ' ' * level @@ -186,6 +253,12 @@ class StepFuncReader(ast.NodeVisitor): self.lines.append(prefix + 'else:') for expr in node.orelse: self.visit_content_in_condition_control(expr, level + 1) + self.generic_visit(node) + + def visit_Raise(self, node, level=0): + prefix = ' ' * level + line = tools.ast2code(ast.fix_missing_locations(node)) + self.lines.append(prefix + line) def visit_Try(self, node): raise errors.CodeError('Do not support "try" handler in Numba backend.') @@ -193,29 +266,27 @@ class StepFuncReader(ast.NodeVisitor): def visit_With(self, node): raise errors.CodeError('Do not support "with" block in Numba backend.') - def visit_Raise(self, node): - raise errors.CodeError('Do not support "raise" statement in Numba backend.') - def visit_Delete(self, node): raise errors.CodeError('Do not support "del" operation in Numba backend.') -def analyze_step_func(f): +def analyze_step_func(host, f): """Analyze the step functions in a population. Parameters ---------- f : callable The step function. + host : Population + The data and the function host. Returns ------- - results : tuple + results : dict The code string of the function, the code scope, the data need pass into the arguments, the data need return. """ - code_string = tools.deindent(inspect.getsource(f)).strip() tree = ast.parse(code_string) @@ -223,15 +294,15 @@ def analyze_step_func(f): # --- args = tools.ast2code(ast.fix_missing_locations(tree.body[0].args)).split(',') - # code lines + # code AST analysis # --- - formatter = StepFuncReader() + formatter = StepFuncReader(host=host) formatter.visit(tree) # data assigned by self.xx in line right # --- self_data_in_right = [] - if args[0] in profile.CLASS_KEYWORDS: + if args[0] in backend.CLASS_KEYWORDS: code = ', \n'.join(formatter.rights) self_data_in_right = re.findall('\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*\\b', code) self_data_in_right = list(set(self_data_in_right)) @@ -241,12 +312,12 @@ def analyze_step_func(f): code = ', \n'.join(formatter.lefts) self_data_without_index_in_left = [] self_data_with_index_in_left = [] - if args[0] in profile.CLASS_KEYWORDS: + if args[0] in backend.CLASS_KEYWORDS: class_p1 = '\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*\\b' self_data_without_index_in_left = set(re.findall(class_p1, code)) class_p2 = '(\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*)\\[.*\\]' - self_data_with_index_in_left = set(re.findall(class_p2, code)) - self_data_without_index_in_left -= self_data_with_index_in_left + self_data_with_index_in_left = set(re.findall(class_p2, code)) - self_data_without_index_in_left + self_data_with_index_in_left = list(self_data_with_index_in_left) self_data_without_index_in_left = list(self_data_without_index_in_left) # code scope @@ -260,8 +331,17 @@ def analyze_step_func(f): self_data_in_right = sorted(self_data_in_right) self_data_without_index_in_left = sorted(self_data_without_index_in_left) self_data_with_index_in_left = sorted(self_data_with_index_in_left) - return code_string, code_scope, self_data_in_right, \ - self_data_without_index_in_left, self_data_with_index_in_left + + analyzed_results = { + 'delay_call': formatter.delay_call, + 'code_string': code_string, + 'code_scope': code_scope, + 'self_data_in_right': self_data_in_right, + 'self_data_without_index_in_left': self_data_without_index_in_left, + 'self_data_with_index_in_left': self_data_with_index_in_left, + } + + return analyzed_results def get_func_body_code(code_string, lambda_func=False): @@ -316,6 +396,8 @@ def get_num_indent(code_string, spaces_per_tab=4): lines = code_string.split('\n') min_indent = 1000 for line in lines: + if line.strip() == '': + continue line = line.replace('\t', ' ' * spaces_per_tab) num_indent = len(line) - len(line.lstrip()) if num_indent < min_indent: @@ -323,84 +405,148 @@ def get_num_indent(code_string, spaces_per_tab=4): return min_indent +def class2func(cls_func, host, func_name=None, show_code=False): + """Transform the function in a class into the ordinary function which is + compatible with the Numba JIT compilation. + + Parameters + ---------- + cls_func : function + The function of the instantiated class. + func_name : str + The function name. If not given, it will get the function by `cls_func.__name__`. + show_code : bool + Whether show the code. + + Returns + ------- + new_func : function + The transformed function. + """ + class_arg, arguments = utils.get_args(cls_func) + func_name = cls_func.__name__ if func_name is None else func_name + host_name = host.name + + # arguments 1 + calls = [] + for arg in arguments: + if hasattr(host, arg): + calls.append(f'{host_name}.{arg}') + elif arg in backend.SYSTEM_KEYWORDS: + calls.append(arg) + else: + raise errors.ModelDefError(f'Step function "{func_name}" of {host} ' + f'define an unknown argument "{arg}" which is not ' + f'an attribute of {host} nor the system keywords ' + f'{backend.SYSTEM_KEYWORDS}.') + + # analysis + analyzed_results = analyze_step_func(host=host, f=cls_func) + delay_call = analyzed_results['delay_call'] + code_string = analyzed_results['code_string'] + code_scope = analyzed_results['code_scope'] + self_data_in_right = analyzed_results['self_data_in_right'] + self_data_without_index_in_left = analyzed_results['self_data_without_index_in_left'] + self_data_with_index_in_left = analyzed_results['self_data_with_index_in_left'] + main_code = get_func_body_code(code_string) + num_indent = get_num_indent(main_code) + data_need_pass = sorted(list(set(self_data_in_right + self_data_with_index_in_left))) + data_need_return = self_data_without_index_in_left + + # check delay + replaces_early = {} + replaces_later = {} + if len(delay_call) > 0: + for delay_ in delay_call.values(): + # delay_ = dict(type=calls[-1], + # args=args, + # keywords=keywords, + # kws_append=kws_append, + # func=func, + # org_call=org_call, + # rep_call=rep_call, + # data_need_pass=data_need_pass) + if delay_['type'] == 'push': + if len(delay_['args'] + delay_['keywords']) == 2: + func = numba.njit(delay.push_type2) + elif len(delay_['args'] + delay_['keywords']) == 1: + func = numba.njit(delay.push_type1) + else: + raise ValueError(f'Unknown delay push. {delay_}') + else: + if len(delay_['args'] + delay_['keywords']) == 1: + func = numba.njit(delay.pull_type1) + elif len(delay_['args'] + delay_['keywords']) == 0: + func = numba.njit(delay.pull_type0) + else: + raise ValueError(f'Unknown delay pull. {delay_}') + delay_call_name = delay_['func'] + data_need_pass.remove(delay_call_name) + data_need_pass.extend(delay_['data_need_pass']) + replaces_early[delay_['org_call']] = delay_['rep_call'] + replaces_later[delay_call_name] = delay_call_name.replace('.', '_') + code_scope[delay_call_name.replace('.', '_')] = func + for target, dest in replaces_early.items(): + main_code = main_code.replace(target, dest) + # main_code = tools.word_replace(main_code, replaces_early) + + # arguments 2: data need pass + new_args = arguments + [] + for data in sorted(set(data_need_pass)): + splits = data.split('.') + replaces_later[data] = data.replace('.', '_') + obj = host + for attr in splits[1:]: + obj = getattr(obj, attr) + if callable(obj): + code_scope[data.replace('.', '_')] = obj + continue + new_args.append(data.replace('.', '_')) + calls.append('.'.join([host_name] + splits[1:])) + + # data need return + assigns = [] + returns = [] + for data in data_need_return: + splits = data.split('.') + assigns.append('.'.join([host_name] + splits[1:])) + returns.append(data.replace('.', '_')) + replaces_later[data] = data.replace('.', '_') + + # code scope + code_scope[host_name] = host + + # codes + main_code = f'def new_{func_name}({", ".join(new_args)}):\n' + main_code + if len(returns): + main_code += f'\n{" " * num_indent}return {", ".join(returns)}' + main_code = tools.word_replace(main_code, replaces_later) + if show_code: + print(main_code) + print(code_scope) + print() + + # recompile + exec(compile(main_code, '', 'exec'), code_scope) + func = code_scope[f'new_{func_name}'] + func = numba.jit(**NUMBA_PROFILE)(func) + return func, calls, assigns + + class NumbaCPUNodeRunner(GeneralNodeRunner): def get_steps_func(self, show_code=False): - for step in self.steps: - func_name = step.__name__ - class_arg, arguments = utils.get_args(step) - host_name = self.host.name - - # arguments 1 - calls = [] - for arg in arguments: - if hasattr(self.host, arg): - calls.append(f'{host_name}.{arg}') - elif arg in backend.SYSTEM_KEYWORDS: - calls.append(arg) - else: - raise errors.ModelDefError(f'Step function "{func_name}" of {self.host} ' - f'define an unknown argument "{arg}" which is not ' - f'an attribute of {self.host} nor the system keywords ' - f'{backend.SYSTEM_KEYWORDS}.') - - # analysis - code_string, code_scope, self_data_in_right, \ - self_data_without_index_in_left, self_data_with_index_in_left = analyze_step_func(step) - main_code = get_func_body_code(code_string) - num_indent = get_num_indent(main_code) - - # arguments 1: data need pass - data_need_pass = sorted(list(set(self_data_in_right + self_data_with_index_in_left))) - replaces = {} - new_args = arguments + [] - for data in data_need_pass: - splits = data.split('.') - if len(splits) == 2: - attr_name = splits[1] - attr_ = getattr(self.host, attr_name) - if callable(attr_): - replaces[data] = data.replace('.', '_') - code_scope[data.replace('.', '_')] = attr_ - continue - new_args.append(data.replace('.', '_')) - calls.append('.'.join([host_name] + splits[1:])) - replaces[data] = data.replace('.', '_') - - # data need return - assigns = [] - returns = [] - for data in self_data_without_index_in_left: - splits = data.split('.') - assigns.append('.'.join([host_name] + splits[1:])) - returns.append(data.replace('.', '_')) - replaces[data] = data.replace('.', '_') - - # code scope - code_scope[host_name] = self.host - - # codes - main_code = f'def new_{func_name}({", ".join(new_args)}):\n' + main_code - if len(returns): - main_code += f'\n{" " * num_indent}return {", ".join(returns)}' - main_code = tools.word_replace(main_code, replaces) - if show_code: - print(main_code) - print(code_scope) - print() - - # recompile - exec(compile(main_code, '', 'exec'), code_scope) - func = code_scope[f'new_{func_name}'] - func = numba.jit(**NUMBA_PROFILE)(func) - self.set_data(f'new_{func_name}', func) + for func_name, step in self.steps.items(): + host = step.__self__ + func, calls, assigns = class2func(cls_func=step, host=host, func_name=func_name, show_code=show_code) + # self.set_data(f'new_{func_name}', func) + setattr(host, f'new_{func_name}', func) # finale - r_line = '' + assignment_line = '' if len(assigns): - r_line = f'{", ".join(assigns)} = ' + assignment_line = f'{", ".join(assigns)} = ' self.formatted_funcs[func_name] = { 'func': func, - 'scope': {host_name: self.host, f'{host_name}_{func_name}': func}, - # 'call': [f'{r_line}{host_name}.new_{func_name}({", ".join(calls)})'] - 'call': [f'{r_line}{host_name}_{func_name}({", ".join(calls)})'] + 'scope': {host.name: host}, + 'call': [f'{assignment_line}{host.name}.new_{func_name}({", ".join(calls)})'] } diff --git a/brainpy/backend/runners/numba_cuda_runner.py b/brainpy/backend/runners/numba_cuda_runner.py index 5d9cb8e6..add5f9c4 100644 --- a/brainpy/backend/runners/numba_cuda_runner.py +++ b/brainpy/backend/runners/numba_cuda_runner.py @@ -1,13 +1,158 @@ # -*- coding: utf-8 -*- +import ast + +from brainpy import backend +from brainpy import tools +from brainpy.simulation.population import SynConn, NeuGroup from .numba_cpu_runner import NumbaCPUNodeRunner +from .numba_cpu_runner import StepFuncReader __all__ = [ 'NumbaCudaNodeRunner', ] +class CudaStepFuncReader(StepFuncReader): + def __init__(self, host): + super(CudaStepFuncReader, self).__init__(host=host) + + self.need_add_cuda = False + # get pre assignment + self.pre_assign = [] + # get post assignment + self.post_assign = [] + + def check_atomic_ops(self, target): + if isinstance(self.host, SynConn) and isinstance(target, ast.Subscript): + values = self.visit_attr(target.value) + slice_ = tools.ast2code(ast.fix_missing_locations(target.slice)) + if len(values) >= 3 and values[-1] in backend.CLASS_KEYWORDS: + obj = getattr(self.host, values[-2]) + if isinstance(obj, NeuGroup): + target_ = '.'.join(values[::-1]) + return target_, slice_ + return None + + def visit_Assign(self, node, level=0): + self.generic_visit(node) + prefix = ' ' * level + expr = tools.ast2code(ast.fix_missing_locations(node.value)) + self.rights.append(expr) + + check = None + if len(node.targets) == 1: + check = self.check_atomic_ops(node.targets[0]) + + if check is None: + targets = [] + for target in node.targets: + targets.append(tools.ast2code(ast.fix_missing_locations(target))) + _target = ' = '.join(targets) + self.lefts.append(_target) + self.lines.append(f'{prefix}{_target} = {expr}') + else: + target, slice_ = check + self.lefts.append(target) + self.lines.append(f'{prefix}cuda.atomic.add({target}, {slice_}, {expr})') + + def visit_AugAssign(self, node, level=0): + self.generic_visit(node) + prefix = ' ' * level + op = tools.ast2code(ast.fix_missing_locations(node.op)) + expr = tools.ast2code(ast.fix_missing_locations(node.value)) + + check = self.check_atomic_ops(node.target) + if check is None: + target = tools.ast2code(ast.fix_missing_locations(node.target)) + self.lefts.append(target) + self.rights.append(expr) + self.lines.append(f"{prefix}{target} {op}= {expr}") + else: + if op == '+': + expr = expr + elif op == '-': + expr = '-' + expr + else: + raise ValueError + target, slice_ = check + self.lefts.append(target) + self.lines.append(f'{prefix}cuda.atomic.add({target}, {slice_}, {expr})') + + +def analyze_step_func(f, host): + """Analyze the step functions in a population. + + Parameters + ---------- + f : callable + The step function. + host : Population + The data and the function host. + + Returns + ------- + results : dict + The code string of the function, the code scope, + the data need pass into the arguments, + the data need return. + """ + + code_string = tools.deindent(inspect.getsource(f)).strip() + tree = ast.parse(code_string) + + # arguments + # --- + args = tools.ast2code(ast.fix_missing_locations(tree.body[0].args)).split(',') + + # code lines + # --- + formatter = StepFuncReader(host=host) + formatter.visit(tree) + + # data assigned by self.xx in line right + # --- + self_data_in_right = [] + if args[0] in backend.CLASS_KEYWORDS: + code = ', \n'.join(formatter.rights) + self_data_in_right = re.findall('\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*\\b', code) + self_data_in_right = list(set(self_data_in_right)) + + # data assigned by self.xxx in line left + # --- + code = ', \n'.join(formatter.lefts) + self_data_without_index_in_left = [] + self_data_with_index_in_left = [] + if args[0] in backend.CLASS_KEYWORDS: + class_p1 = '\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*\\b' + self_data_without_index_in_left = set(re.findall(class_p1, code)) + class_p2 = '(\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*)\\[.*\\]' + self_data_with_index_in_left = set(re.findall(class_p2, code)) + self_data_without_index_in_left -= self_data_with_index_in_left + self_data_without_index_in_left = list(self_data_without_index_in_left) + + # code scope + # --- + closure_vars = inspect.getclosurevars(f) + code_scope = dict(closure_vars.nonlocals) + code_scope.update(closure_vars.globals) + + # final + # --- + self_data_in_right = sorted(self_data_in_right) + self_data_without_index_in_left = sorted(self_data_without_index_in_left) + self_data_with_index_in_left = sorted(self_data_with_index_in_left) + + analyzed_results = { + 'code_string': code_string, + 'code_scope': code_scope, + 'self_data_in_right': self_data_in_right, + 'self_data_without_index_in_left': self_data_without_index_in_left, + 'self_data_with_index_in_left': self_data_with_index_in_left, + } + + return analyzed_results + class NumbaCudaNodeRunner(NumbaCPUNodeRunner): pass - diff --git a/brainpy/backend/runners/utils.py b/brainpy/backend/runners/utils.py index f1214e86..afa82567 100644 --- a/brainpy/backend/runners/utils.py +++ b/brainpy/backend/runners/utils.py @@ -3,8 +3,8 @@ import inspect +from brainpy import backend from brainpy import errors -from brainpy import profile __all__ = [ 'get_args' @@ -47,11 +47,11 @@ def get_args(f): # 2. check the function arguments class_kw = None - if arguments[0] in profile.CLASS_KEYWORDS: + if len(arguments) > 0 and arguments[0] in backend.CLASS_KEYWORDS: class_kw = arguments[0] arguments = arguments[1:] for a in arguments: - if a in profile.CLASS_KEYWORDS: + if a in backend.CLASS_KEYWORDS: raise errors.DiffEqError(f'Class keywords "{a}" must be defined ' f'as the first argument.') return class_kw, arguments diff --git a/brainpy/connectivity/base.py b/brainpy/connectivity/base.py index 375e5843..9c33c69d 100644 --- a/brainpy/connectivity/base.py +++ b/brainpy/connectivity/base.py @@ -317,6 +317,12 @@ def post_slice_syn(i, j, num_post=None): return pre_ids, post_ids, slicing +SUPPORTED_SYN_STRUCTURE = ['pre_ids', 'post_ids', 'conn_mat', + 'pre2post', 'post2pre', + 'pre2syn', 'post2syn', + 'pre_slice_syn', 'post_slice_syn'] + + class AbstractConnector(abc.ABC): def __call__(self, *args, **kwargs): pass @@ -345,22 +351,23 @@ class Connector(AbstractConnector): # synaptic weights self.weights = None - def requires(self, syn_requires): + def requires(self, *syn_requires): # get synaptic requires requires = set() for n in syn_requires: - if n in ['pre_ids', 'post_ids', 'conn_mat', - 'pre2post', 'post2pre', - 'pre2syn', 'post2syn', - 'pre_slice_syn', 'post_slice_syn']: + if n in SUPPORTED_SYN_STRUCTURE: requires.add(n) + else: + raise ValueError(f'Unknown synapse structure {n}. We only support ' + f'{SUPPORTED_SYN_STRUCTURE}.') requires = list(requires) # synaptic structure to handle needs = [] if 'pre_slice_syn' in requires and 'post_slice_syn' in requires: raise errors.ModelUseError('Cannot use "pre_slice_syn" and "post_slice_syn" ' - 'simultaneously. \nWe recommend you use "pre_slice_syn + ' + 'simultaneously. \n' + 'We recommend you use "pre_slice_syn + ' 'post2syn" or "post_slice_syn + pre2syn".') elif 'pre_slice_syn' in requires: needs.append('pre_slice_syn') @@ -375,6 +382,12 @@ class Connector(AbstractConnector): for n in needs: getattr(self, f'make_{n}')() + # returns + if len(requires) == 1: + return getattr(self, requires[0]) + else: + return tuple([getattr(self, r) for r in requires]) + def make_conn_mat(self): if self.conn_mat is None: self.conn_mat = ij2mat(self.pre_ids, self.post_ids, self.num_pre, self.num_post) diff --git a/brainpy/connectivity/methods.py b/brainpy/connectivity/methods.py index 69f331eb..5280c45d 100644 --- a/brainpy/connectivity/methods.py +++ b/brainpy/connectivity/methods.py @@ -221,6 +221,7 @@ class One2One(Connector): self.pre_ids = backend.arange(length) self.post_ids = backend.arange(length) + return self one2one = One2One() @@ -246,7 +247,11 @@ class All2All(Connector): if not self.include_self: eye = np.arange(min([pre_len, post_len])) self.conn_mat[eye, eye] = 0 + pre_ids, post_ids = np.where(mat > 0) + self.pre_ids = backend.as_tensor(np.ascontiguousarray(pre_ids)) + self.post_ids = backend.as_tensor(np.ascontiguousarray(post_ids)) self.conn_mat = backend.as_tensor(mat) + return self all2all = All2All(include_self=True) @@ -285,6 +290,7 @@ class GridFour(Connector): conn_j.extend(a[1]) self.pre_ids = backend.as_tensor(conn_i) self.post_ids = backend.as_tensor(conn_j) + return self grid_four = GridFour() @@ -345,6 +351,7 @@ class GridN(Connector): conn_j.extend(res[1]) self.pre_ids = backend.as_tensor(conn_i) self.post_ids = backend.as_tensor(conn_j) + return self class GridEight(GridN): @@ -389,6 +396,7 @@ class FixedProb(Connector): self.conn_mat = backend.as_tensor(conn_mat) self.pre_ids = backend.as_tensor(np.ascontiguousarray(pre_ids)) self.post_ids = backend.as_tensor(np.ascontiguousarray(post_ids)) + return self class FixedPreNum(Connector): @@ -432,6 +440,7 @@ class FixedPreNum(Connector): post_ids = np.asarray(np.repeat(np.arange(num_post), num_pre), dtype=np.int_) self.pre_ids = backend.as_tensor(pre_ids) self.post_ids = backend.as_tensor(post_ids) + return self class FixedPostNum(Connector): @@ -477,6 +486,7 @@ class FixedPostNum(Connector): pre_ids = np.asarray(np.repeat(np.arange(num_pre), num_post), dtype=np.int64) self.pre_ids = backend.as_tensor(pre_ids) self.post_ids = backend.as_tensor(post_ids) + return self class GaussianWeight(Connector): @@ -552,6 +562,7 @@ class GaussianWeight(Connector): self.pre_ids = backend.as_tensor(pre_ids) self.post_ids = backend.as_tensor(post_ids) self.weights = backend.as_tensor(w) + return self class GaussianProb(Connector): @@ -616,6 +627,7 @@ class GaussianProb(Connector): j = np.asarray(j, dtype=np.int_)[selected_idxs] self.pre_ids = backend.as_tensor(i) self.post_ids = backend.as_tensor(j) + return self class DOG(Connector): @@ -689,6 +701,7 @@ class DOG(Connector): self.pre_ids = backend.as_tensor(i) self.post_ids = backend.as_tensor(j) self.weights = backend.as_tensor(w) + return self class ScaleFree(Connector): diff --git a/brainpy/inputs.py b/brainpy/inputs.py index 6490c63b..62df0797 100644 --- a/brainpy/inputs.py +++ b/brainpy/inputs.py @@ -2,7 +2,7 @@ import numpy as np -from brainpy import profile +from brainpy import backend __all__ = [ 'constant_current', @@ -34,7 +34,7 @@ def constant_current(Iext, dt=None): current_and_duration : tuple (The formatted current, total duration) """ - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt # get input current dimension, shape, and duration I_duration = 0. @@ -89,7 +89,7 @@ def spike_current(points, lengths, sizes, duration, dt=None): current_and_duration : tuple (The formatted current, total duration) """ - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt assert isinstance(points, (list, tuple)) if isinstance(lengths, (float, int)): lengths = [lengths] * len(points) @@ -126,7 +126,7 @@ def ramp_current(c_start, c_end, duration, t_start=0, t_end=None, dt=None): current_and_duration : tuple (The formatted current, total duration) """ - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt t_end = duration if t_end is None else t_end current = np.zeros(int(np.ceil(duration / dt))) diff --git a/brainpy/integrators/__init__.py b/brainpy/integrators/__init__.py index 2305f132..a8b91803 100644 --- a/brainpy/integrators/__init__.py +++ b/brainpy/integrators/__init__.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from . import ode -from . import sde from . import dde from . import fde +from . import ode +from . import sde +from .constants import * from .delay_vars import * from .integrate_wrapper import * -from .constants import * diff --git a/brainpy/integrators/ast_analysis.py b/brainpy/integrators/ast_analysis.py index fb502893..e04027cd 100644 --- a/brainpy/integrators/ast_analysis.py +++ b/brainpy/integrators/ast_analysis.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- import ast +import inspect +from collections import OrderedDict from brainpy import errors from brainpy import tools - __all__ = [ 'DiffEqReader', 'separate_variables', @@ -123,13 +124,13 @@ class DiffEqReader(ast.NodeVisitor): f'analyze "del" operation in differential equation.') -def separate_variables(returns, variables, right_exprs, code_lines): +def separate_variables(func_or_code): """Separate the expressions in a differential equation for each variable. For example, take the HH neuron model as an example: >>> eq_code = ''' - >>> def integral(V, m, h, n, t, Iext): + >>> def integral(m, h, t, Iext, V): >>> alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) >>> beta = 4.0 * np.exp(-(V + 65) / 18) >>> dmdt = alpha * (1 - m) - beta * m @@ -154,40 +155,66 @@ def separate_variables(returns, variables, right_exprs, code_lines): Parameters ---------- - returns : list of str - The return expressions. - variables : list of list - The variables on each code line. - right_exprs : list of str - The right expression for each code line. - code_lines : list of str - The code lines in the differential equations. + func_or_code : callable, str + The callable function or the function code. Returns ------- - expressions_for_returns : dict + anlysis : dict The expressions for each return variable. """ - return_requires = {r: tools.get_identifiers(r) for r in returns} - expressions_for_returns = {r: [] for r in returns} + if callable(func_or_code): + func_or_code = tools.deindent(inspect.getsource(func_or_code)) + assert isinstance(func_or_code, str) + analyser = DiffEqReader() + analyser.visit(ast.parse(func_or_code)) + + returns = analyser.returns + variables = analyser.variables + right_exprs = analyser.rights + code_lines = analyser.code_lines + + return_requires = OrderedDict([(r, set(tools.get_identifiers(r))) for r in returns]) + code_lines_for_returns = OrderedDict([(r, []) for r in returns]) + variables_for_returns = OrderedDict([(r, []) for r in returns]) + expressions_for_returns = OrderedDict([(r, []) for r in returns]) length = len(variables) - reverse_ids = [i-length for i in range(length)] - reverse_ids = reverse_ids[::-1] - for r in expressions_for_returns.keys(): + reverse_ids = list(reversed([i - length for i in range(length)])) + for r in code_lines_for_returns.keys(): for rid in reverse_ids: dep = [] for v in variables[rid]: if v in return_requires[r]: dep.append(v) if len(dep): - expressions_for_returns[r].append(code_lines[rid]) + code_lines_for_returns[r].append(code_lines[rid]) + variables_for_returns[r].append(variables[rid]) expr = right_exprs[rid] - return_requires[r].update(tools.get_identifiers(expr)) + expressions_for_returns[r].append(expr) for d in dep: return_requires[r].remove(d) - for r, v in expressions_for_returns.items(): - expressions_for_returns[r] = v[::-1] - - return expressions_for_returns - + return_requires[r].update(tools.get_identifiers(expr)) + for r in list(code_lines_for_returns.keys()): + code_lines_for_returns[r] = code_lines_for_returns[r][::-1] + variables_for_returns[r] = variables_for_returns[r][::-1] + expressions_for_returns[r] = expressions_for_returns[r][::-1] + + analysis = tools.DictPlus( + code_lines_for_returns=code_lines_for_returns, + variables_for_returns=variables_for_returns, + expressions_for_returns=expressions_for_returns, + ) + return analysis + + +# def dissect_diff_eq(func_or_code): +# if callable(func_or_code): +# func_or_code = tools.deindent(inspect.getsource(func_or_code)) +# assert isinstance(func_or_code, str) +# analyser = DiffEqReader() +# analyser.visit(ast.parse(func_or_code)) +# return separate_variables(returns=analyser.returns, +# variables=analyser.variables, +# right_exprs=analyser.rights, +# code_lines=analyser.code_lines) diff --git a/brainpy/integrators/delay_vars.py b/brainpy/integrators/delay_vars.py index 3d26b0ef..bd5ee47c 100644 --- a/brainpy/integrators/delay_vars.py +++ b/brainpy/integrators/delay_vars.py @@ -5,7 +5,6 @@ import abc import math from brainpy import backend -from brainpy import profile __all__ = [ 'AbstractDelay', @@ -24,36 +23,44 @@ class AbstractDelay(abc.ABC): class ConstantDelay(AbstractDelay): - def __init__(self, size, delay_len, before_t0): - # check size - if isinstance(size, int): - size = (size,) - if not isinstance(size, (tuple, list)): - raise ValueError('"size" must be an int, or a list/tuple of int.') + def __init__(self, v0, delay_len, before_t0=0., t0=0., dt=None): + # size + self.size = backend.shape(v0) - # check delay_len - dt = profile.get_dt() - num_delay = int(math.ceil(delay_len / dt)) + # delay_len + self.delay_len = delay_len + self.dt = backend.get_dt() if dt is None else dt + self.num_delay = int(math.ceil(delay_len / self.dt)) - # delay data - self.data = backend.zeros((num_delay,) + size) + # other variables + self._delay_in = self.num_delay - 1 + self._delay_out = 0 + self.current_time = t0 + + # before_t0 + self.before_t0 = before_t0 - # check defore_t0 + # delay data + self.data = backend.zeros((self.num_delay + 1,) + self.size) if callable(before_t0): - for i in range(num_delay): - self.data[i] = before_t0((i - num_delay) * dt) + for i in range(self.num_delay): + self.data[i] = before_t0(t0 + (i - self.num_delay) * self.dt) else: - self.data[:] = before_t0 + self.data[:-1] = before_t0 + self.data[-1] = v0 - # other variables - self._delay_in = 0 - self._delay_out = ... + def __setitem__(self, time, value): # push + self.data[self._delay_in] = value + self.current_time = time - def __setitem__(self, time, value): - pass + def __getitem__(self, time): # pull + diff = self.current_time - time + m = math.ceil(diff / self.dt) + return self.data[self._delay_out] - def __getitem__(self, time): - pass + def update(self): + self._delay_in = (self._delay_in + 1) % self.num_delay + self._delay_out = (self._delay_out + 1) % self.num_delay class VaryingDelay(AbstractDelay): diff --git a/brainpy/integrators/integrate_wrapper.py b/brainpy/integrators/integrate_wrapper.py index b5307233..3083ddca 100644 --- a/brainpy/integrators/integrate_wrapper.py +++ b/brainpy/integrators/integrate_wrapper.py @@ -8,49 +8,109 @@ __all__ = [ 'sdeint', 'ddeint', 'fdeint', + + 'set_default_odeint', + 'get_default_odeint', + 'set_default_sdeint', + 'get_default_sdeint', ] -supported_ode = [m for m in dir(ode) if not m.startswith('__')] -supported_sde = [m for m in dir(sde) if not m.startswith('__')] +_DEFAULT_ODE_METHOD = 'euler' +_DEFAULT_SDE_METHOD = 'euler' +SUPPORTED_ODE = [m for m in dir(ode) if not m.startswith('__')] +SUPPORTED_SDE = [m for m in dir(sde) if not m.startswith('__')] -def odeint(f=None, method=None, **kwargs): - def wrapper(f, ode_type, **kwargs): - integrator = getattr(ode, ode_type) - return integrator(f, **kwargs) +def _wrapper(f, method, module, **kwargs): + integrator = getattr(module, method) + return integrator(f, **kwargs) + +def odeint(f=None, method=None, **kwargs): if method is None: - method = 'euler' - if method not in supported_ode: + method = _DEFAULT_ODE_METHOD + if method not in SUPPORTED_ODE: raise ValueError(f'Unknown ODE numerical method "{method}". Currently ' - f'BrainPy only support: {supported_ode}') + f'BrainPy only support: {SUPPORTED_ODE}') if f is None: - return lambda f: wrapper(f, method, **kwargs) + return lambda f: _wrapper(f, method=method, module=ode, **kwargs) else: - return wrapper(f, method, **kwargs) + return _wrapper(f, method=method, module=ode, **kwargs) def sdeint(f=None, method=None, **kwargs): - def wrapper(f, ode_type, **kwargs): - integrator = getattr(sde, ode_type) - return integrator(f, **kwargs) - if method is None: - method = 'euler' - if method not in supported_sde: + method = _DEFAULT_SDE_METHOD + if method not in SUPPORTED_SDE: raise ValueError(f'Unknown SDE numerical method "{method}". Currently ' - f'BrainPy only support: {supported_sde}') + f'BrainPy only support: {SUPPORTED_SDE}') if f is None: - return lambda f: wrapper(f, method, **kwargs) + return lambda f: _wrapper(f, method=method, module=sde, **kwargs) else: - return wrapper(f, method, **kwargs) + return _wrapper(f, method=method, module=sde, **kwargs) def ddeint(): - pass + raise NotImplementedError def fdeint(): - pass + raise NotImplementedError + + +def set_default_odeint(method): + """Set the default ODE numerical integrator method for differential equations. + + Parameters + ---------- + method : str, callable + Numerical integrator method. + """ + if not isinstance(method, str): + raise ValueError(f'Only support string, not {type(method)}.') + if method not in SUPPORTED_ODE: + raise ValueError(f'Unsupported ODE numerical method: {method}.') + + global _DEFAULT_ODE_METHOD + _DEFAULT_ODE_METHOD = method + + +def get_default_odeint(): + """Get the default ODE numerical integrator method. + + Returns + ------- + method : str + The default numerical integrator method. + """ + return _DEFAULT_ODE_METHOD + + +def set_default_sdeint(method): + """Set the default SDE numerical integrator method for differential equations. + + Parameters + ---------- + method : str, callable + Numerical integrator method. + """ + if not isinstance(method, str): + raise ValueError(f'Only support string, not {type(method)}.') + if method not in SUPPORTED_SDE: + raise ValueError(f'Unsupported SDE numerical method: {method}.') + + global _DEFAULT_SDE_METHOD + _DEFAULT_SDE_METHOD = method + + +def get_default_sdeint(): + """Get the default ODE numerical integrator method. + + Returns + ------- + method : str + The default numerical integrator method. + """ + return _DEFAULT_SDE_METHOD diff --git a/brainpy/integrators/ode/exp_euler.py b/brainpy/integrators/ode/exp_euler.py index 1ac741d9..13358e69 100644 --- a/brainpy/integrators/ode/exp_euler.py +++ b/brainpy/integrators/ode/exp_euler.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from brainpy import profile from brainpy import backend __all__ = [ @@ -9,7 +8,7 @@ __all__ = [ def exponential_euler(f, return_linear_term=False): - dt = profile.get_dt() + dt = backend.get_dt() def int_f(x, t, *args): df, linear_part = f(x, t, *args) diff --git a/brainpy/integrators/ode/rk_adaptive_methods.py b/brainpy/integrators/ode/rk_adaptive_methods.py index 7e3c1a8b..8ef27cbc 100644 --- a/brainpy/integrators/ode/rk_adaptive_methods.py +++ b/brainpy/integrators/ode/rk_adaptive_methods.py @@ -5,9 +5,9 @@ https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods """ -from brainpy import profile +from brainpy import backend +from brainpy.integrators import constants from .wrapper import adaptive_rk_wrapper -from brainpy.integrators import utils __all__ = [ 'rkf45', @@ -41,10 +41,10 @@ def _base(A, B1, B2, C, f=None, tol=None, adaptive=None, """ adaptive = False if (adaptive is None) else adaptive - dt = profile.get_dt() if (dt is None) else dt + dt = backend.get_dt() if (dt is None) else dt tol = 0.1 if tol is None else tol show_code = False if tol is None else show_code - var_type = utils.POPU_VAR if var_type is None else var_type + var_type = constants.POPU_VAR if var_type is None else var_type if f is None: return lambda f: adaptive_rk_wrapper(f, dt=dt, A=A, B1=B1, B2=B2, C=C, tol=tol, diff --git a/brainpy/integrators/ode/rk_methods.py b/brainpy/integrators/ode/rk_methods.py index 152a124f..8f2b6116 100644 --- a/brainpy/integrators/ode/rk_methods.py +++ b/brainpy/integrators/ode/rk_methods.py @@ -4,9 +4,9 @@ https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods#Kutta's_third-order_method """ -from brainpy import profile -from brainpy.integrators import utils +from brainpy import backend from .wrapper import rk_wrapper +from .wrapper import wrapper_of_rk2 __all__ = [ 'euler', @@ -25,7 +25,7 @@ __all__ = [ def _base(A, B, C, f, show_code, dt): - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt show_code = False if show_code is None else show_code if f is None: @@ -126,34 +126,6 @@ def ralston2(f=None, show_code=None, dt=None): return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) -def _rk2_wrapper(f, show_code, dt, beta): - vars, other_args, org_args = utils.get_args(f) - - code_scope = {f.__name__: f, 'dt': dt, 'beta': beta, - 'k1': 1 - 1 / (2 * beta), 'k2': 1 / (2 * beta)} - code_lines = [f'def int_{f.__name__}({", ".join(org_args)}):'] - # k1 - k1_args = vars + other_args - k1_vars_d = [f'd{v}_k1' for v in vars] - code_lines.append(f' {", ".join(k1_vars_d)} = {f.__name__}({", ".join(k1_args)})') - # k2 - k2_args = [f'{v} + d{v}_k1 * dt * beta' for v in vars] - k2_args.append('t + dt * beta') - k2_args.extend(other_args[1:]) - k2_vars_d = [f'd{v}_k2' for v in vars] - code_lines.append(f' {", ".join(k2_vars_d)} = {f.__name__}({", ".join(k2_args)})') - # returns - for v, k1, k2 in zip(vars, k1_vars_d, k2_vars_d): - code_lines.append(f' {v}_new = {v} + ({k1} * k1 + {k2} * k2) * dt') - return_vars = [f'{v}_new' for v in vars] - code_lines.append(f' return {", ".join(return_vars)}') - - code = '\n'.join(code_lines) - if show_code: - print(code) - print(code_scope) - exec(compile(code, '', 'exec'), code_scope) - return code_scope[f'int_{f.__name__}'] def rk2(f=None, show_code=None, dt=None, beta=None): @@ -176,13 +148,13 @@ def rk2(f=None, show_code=None, dt=None, beta=None): \\end{array} """ beta = 2 / 3 if beta is None else beta - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt show_code = False if show_code is None else show_code if f is None: - return lambda f: _rk2_wrapper(f, show_code=show_code, dt=dt, beta=beta) + return lambda f: wrapper_of_rk2(f, show_code=show_code, dt=dt, beta=beta) else: - return _rk2_wrapper(f, show_code=show_code, dt=dt, beta=beta) + return wrapper_of_rk2(f, show_code=show_code, dt=dt, beta=beta) def rk3(f=None, show_code=None, dt=None): diff --git a/brainpy/integrators/ode/wrapper.py b/brainpy/integrators/ode/wrapper.py index b0cde1ab..fe9d9e49 100644 --- a/brainpy/integrators/ode/wrapper.py +++ b/brainpy/integrators/ode/wrapper.py @@ -1,16 +1,31 @@ # -*- coding: utf-8 -*- +from pprint import pprint + +from brainpy.integrators import constants from brainpy.integrators import utils __all__ = [ 'rk_wrapper', 'adaptive_rk_wrapper', + 'wrapper_of_rk2', ] _ODE_UNKNOWN_NO = 0 -def _step(vars, dt_var, f_name, A, C, code_lines, other_args): +def _f_names(f): + if f.__name__.isidentifier(): + f_name = f.__name__ + else: + global _ODE_UNKNOWN_NO + f_name = f'ode_unknown_{_ODE_UNKNOWN_NO}' + _ODE_UNKNOWN_NO += 1 + f_new_name = constants.NAME_PREFIX + f_name + return f_new_name + + +def _step(vars, dt_var, A, C, code_lines, other_args): # steps for si, sval in enumerate(A): # k-step arguments @@ -46,7 +61,7 @@ def _step(vars, dt_var, f_name, A, C, code_lines, other_args): k_derivatives = [f'd{v}_k{si + 1}' for v in vars] # k-step code line - code_lines.append(f' {", ".join(k_derivatives)} = {f_name}(' + code_lines.append(f' {", ".join(k_derivatives)} = f(' f'{", ".join(k_args + other_args[1:])})') @@ -62,14 +77,26 @@ def _update(vars, dt_var, B, code_lines): return return_args -def _compile(code_lines, code_scope, show_code): +def _compile_and_assign_attrs(code_lines, code_scope, show_code, + func_name, variables, parameters, dt): + # compile code = '\n'.join(code_lines) if show_code: print(code) - print(code_scope) print() + pprint(code_scope) + print() + utils.numba_func(code_scope, ['f']) exec(compile(code, '', 'exec'), code_scope) - return code_scope + + # attribute assignment + new_f = code_scope[func_name] + new_f.variables = variables + new_f.parameters = parameters + new_f.origin_f = code_scope['f'] + new_f.dt = dt + utils.numba_func(code_scope, func_name) + return code_scope[func_name] def rk_wrapper(f, show_code, dt, A, B, C): @@ -124,21 +151,17 @@ def rk_wrapper(f, show_code, dt, A, B, C): """ class_kw, variables, parameters, arguments = utils.get_args(f) dt_var = 'dt' - if f.__name__.isdentifier(): - f_name = f.__name__ - else: - global _ODE_UNKNOWN_NO - f_name = f'ode_unknown_{_ODE_UNKNOWN_NO}' - _ODE_UNKNOWN_NO += 1 - f_new_name = utils.NAME_PREFIX + f_name + func_name = _f_names(f) # code scope - code_scope = {f_name: f, 'dt': dt} + code_scope = {'f': f, 'dt': dt} # code lines - code_lines = [f'def {f_new_name}({", ".join(arguments)}):'] + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + # step stage - _step(variables, dt_var, f_name, A, C, code_lines, parameters) + _step(variables, dt_var, A, C, code_lines, parameters) + # variable update return_args = _update(variables, dt_var, B, code_lines) @@ -146,8 +169,9 @@ def rk_wrapper(f, show_code, dt, A, B, C): code_lines.append(f' return {", ".join(return_args)}') # compilation - _compile(code_lines, code_scope, show_code) - return code_scope[f_new_name] + return _compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + func_name=func_name, variables=variables, parameters=parameters, dt=dt) def adaptive_rk_wrapper(f, dt, A, B1, B2, C, tol, adaptive, show_code, var_type): @@ -211,31 +235,26 @@ def adaptive_rk_wrapper(f, dt, A, B1, B2, C, tol, adaptive, show_code, var_type) integral_func : callable The one-step numerical integration function. """ - assert var_type in utils.SUPPORTED_VAR_TYPE, \ - f'"var_type" only supports {utils.SUPPORTED_VAR_TYPE}, not {var_type}.' + assert var_type in constants.SUPPORTED_VAR_TYPE, \ + f'"var_type" only supports {constants.SUPPORTED_VAR_TYPE}, ' \ + f'not {var_type}.' class_kw, variables, parameters, arguments = utils.get_args(f) dt_var = 'dt' - if f.__name__.isdentifier(): - f_name = f.__name__ - else: - global _ODE_UNKNOWN_NO - f_name = f'ode_unknown_{_ODE_UNKNOWN_NO}' - _ODE_UNKNOWN_NO += 1 - f_new_name = utils.NAME_PREFIX + f_name + func_name = _f_names(f) if adaptive: # code scope - code_scope = {f_name: f, 'tol': tol} + code_scope = {'f': f, 'tol': tol} arguments = list(arguments) + ['dt'] else: # code scope - code_scope = {f_name: f, 'dt': dt} + code_scope = {'f': f, 'dt': dt} # code lines - code_lines = [f'def {f_new_name}({", ".join(arguments)}):'] + code_lines = [f'def {func_name}({", ".join(arguments)}):'] # stage steps - _step(variables, dt_var, f_name, A, C, code_lines, parameters) + _step(variables, dt_var, A, C, code_lines, parameters) # variable update return_args = _update(variables, dt_var, B1, code_lines) @@ -253,7 +272,7 @@ def adaptive_rk_wrapper(f, dt, A, B1, B2, C, tol, adaptive, show_code, var_type) if diff != 0.: result.append(f'd{v}_k{i + 1} * {dt_var} * {diff}') if len(result) > 0: - if var_type == utils.SCALAR_VAR: + if var_type == constants.SCALAR_VAR: code_lines.append(f' {v}_te = abs({" + ".join(result)})') else: code_lines.append(f' {v}_te = sum(abs({" + ".join(result)}))') @@ -270,5 +289,35 @@ def adaptive_rk_wrapper(f, dt, A, B1, B2, C, tol, adaptive, show_code, var_type) code_lines.append(f' return {", ".join(return_args)}') # compilation - _compile(code_lines, code_scope, show_code) - return code_scope[f_new_name] + return _compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + func_name=func_name, variables=variables, parameters=parameters, dt=dt) + + +def wrapper_of_rk2(f, show_code, dt, beta): + class_kw, variables, parameters, arguments = utils.get_args(f) + func_name = _f_names(f) + + code_scope = {'f': f, 'dt': dt, 'beta': beta, + 'k1': 1 - 1 / (2 * beta), 'k2': 1 / (2 * beta)} + code_lines = [f'def {func_name}({", ".join(arguments)}):'] + # k1 + k1_args = variables + parameters + k1_vars_d = [f'd{v}_k1' for v in variables] + code_lines.append(f' {", ".join(k1_vars_d)} = f({", ".join(k1_args)})') + # k2 + k2_args = [f'{v} + d{v}_k1 * dt * beta' for v in variables] + k2_args.append('t + dt * beta') + k2_args.extend(parameters[1:]) + k2_vars_d = [f'd{v}_k2' for v in variables] + code_lines.append(f' {", ".join(k2_vars_d)} = f({", ".join(k2_args)})') + # returns + for v, k1, k2 in zip(variables, k1_vars_d, k2_vars_d): + code_lines.append(f' {v}_new = {v} + ({k1} * k1 + {k2} * k2) * dt') + return_vars = [f'{v}_new' for v in variables] + code_lines.append(f' return {", ".join(return_vars)}') + + return _compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + func_name=func_name, variables=variables, parameters=parameters, dt=dt) + diff --git a/brainpy/integrators/sde/common.py b/brainpy/integrators/sde/common.py index fc68ef32..2e3fb849 100644 --- a/brainpy/integrators/sde/common.py +++ b/brainpy/integrators/sde/common.py @@ -19,11 +19,12 @@ def basic_info(f, g): func_name = f'unknown_sde{_SDE_UNKNOWN_NO}' func_new_name = constants.NAME_PREFIX + func_name class_kw, variables, parameters, arguments = utils.get_args(f) - return vdt, variables, parameters, arguments, func_new_name -def return_and_compile(code_lines, code_scope, show_code, variables): +def return_compile_and_assign_attrs(code_lines, code_scope, show_code, + variables, parameters, func_name, + sde_type, var_type, wiener_type, dt): # returns new_vars = [f'{var}_new' for var in variables] code_lines.append(f' return {", ".join(new_vars)}') @@ -35,5 +36,18 @@ def return_and_compile(code_lines, code_scope, show_code, variables): print() pprint(code_scope) print() + utils.numba_func(code_scope, ['f', 'g']) exec(compile(code, '', 'exec'), code_scope) + # attribute assignment + new_f = code_scope[func_name] + new_f.variables = variables + new_f.parameters = parameters + new_f.origin_f = code_scope['f'] + new_f.origin_g = code_scope['g'] + new_f.sde_type = sde_type + new_f.var_type = var_type + new_f.wiener_type = wiener_type + new_f.dt = dt + utils.numba_func(code_scope, func_name) + return code_scope[func_name] diff --git a/brainpy/integrators/sde/euler_and_milstein.py b/brainpy/integrators/sde/euler_and_milstein.py index 14dfcae9..7692c3ff 100644 --- a/brainpy/integrators/sde/euler_and_milstein.py +++ b/brainpy/integrators/sde/euler_and_milstein.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from brainpy import backend -from brainpy import profile from brainpy.integrators import constants from . import common @@ -89,7 +88,7 @@ def _wrap(wrapper, f, g, dt, sde_type, var_type, wiener_type, show_code): f'But we got {wiener_type}.' show_code = False if show_code is None else show_code - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt if f is not None and g is not None: return wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, @@ -180,8 +179,10 @@ def _euler_wrapper(f, g, dt, sde_type, var_type, wiener_type, show_code): f'supports {constants.SUPPORTED_SDE_TYPE}.') # return and compile - common.return_and_compile(code_lines, code_scope, show_code, variables) - return code_scope[func_name] + return common.return_compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + variables=variables, parameters=parameters, func_name=func_name, + sde_type=sde_type, var_type=var_type, wiener_type=wiener_type, dt=dt) def _milstein_wrapper(f, g, dt, sde_type, var_type, wiener_type, show_code): @@ -254,8 +255,10 @@ def _milstein_wrapper(f, g, dt, sde_type, var_type, wiener_type, show_code): code_lines.append(' ') # return and compile - common.return_and_compile(code_lines, code_scope, show_code, variables) - return code_scope[func_name] + return common.return_compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + variables=variables, parameters=parameters, func_name=func_name, + sde_type=sde_type, var_type=var_type, wiener_type=wiener_type, dt=dt) # ------------------ diff --git a/brainpy/integrators/sde/exp_euler.py b/brainpy/integrators/sde/exp_euler.py index f66be932..6fcaea99 100644 --- a/brainpy/integrators/sde/exp_euler.py +++ b/brainpy/integrators/sde/exp_euler.py @@ -3,11 +3,10 @@ import numpy as np import sympy -from brainpy.integrators import ast_analysis from brainpy import backend from brainpy import errors -from brainpy import profile from brainpy import tools +from brainpy.integrators import ast_analysis __all__ = [ 'exponential_euler', @@ -41,7 +40,7 @@ class Integrator(object): # function scope code_scopes = {'numpy': np} for k_, v_ in self.code_scope.items(): - if profile.is_jit() and callable(v_): + if backend.is_jit() and callable(v_): v_ = tools.numba_func(v_) code_scopes[k_] = v_ code_scopes.update(ast_analysis.get_mapping_scope()) @@ -50,7 +49,7 @@ class Integrator(object): # function compilation exec(compile(func_code, '', 'exec'), code_scopes) func = code_scopes[self.py_func_name] - if profile.is_jit(): + if backend.is_jit(): func = tools.jit(func) self._update_func = func @@ -73,7 +72,7 @@ class Integrator(object): @property def code_scope(self): scope = self.diff_eq.func_scope - if profile.run_on_cpu(): + if backend.run_on_cpu(): scope['_normal_like_'] = backend.normal_like return scope @@ -140,7 +139,7 @@ class ExponentialEuler(Integrator): @staticmethod def get_integral_step(diff_eq, *args): - dt = profile.get_dt() + dt = backend.get_dt() f_expressions = diff_eq.get_f_expressions(substitute_vars=diff_eq.var_name) # code lines @@ -205,7 +204,7 @@ class ExponentialEuler(Integrator): def exponential_euler(f): - dt = profile.get_dt() + dt = backend.get_dt() dt_sqrt = dt ** 0.5 def int_f(x, t, *args): diff --git a/brainpy/integrators/sde/srk_scalar.py b/brainpy/integrators/sde/srk_scalar.py index 8be78b8d..2f557ef6 100644 --- a/brainpy/integrators/sde/srk_scalar.py +++ b/brainpy/integrators/sde/srk_scalar.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from brainpy import backend -from brainpy import profile from brainpy.integrators import constants from . import common @@ -126,8 +125,10 @@ def _srk1w1_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): code_lines.append(' ') # return and compile - common.return_and_compile(code_lines, code_scope, show_code, variables) - return code_scope[func_name] + return common.return_compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + variables=variables, parameters=parameters, func_name=func_name, + sde_type=sde_type, var_type=var_type, wiener_type=wiener_type, dt=dt) def _srk2w1_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): @@ -222,8 +223,10 @@ def _srk2w1_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): code_lines.append(' ') # return and compile - common.return_and_compile(code_lines, code_scope, show_code, variables) - return code_scope[func_name] + return common.return_compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + variables=variables, parameters=parameters, func_name=func_name, + sde_type=sde_type, var_type=var_type, wiener_type=wiener_type, dt=dt) def _KlPl_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): @@ -266,8 +269,10 @@ def _KlPl_wrapper(f, g, dt, show_code, sde_type, var_type, wiener_type): code_lines.append(' ') # return and compile - common.return_and_compile(code_lines, code_scope, show_code, variables) - return code_scope[func_name] + return common.return_compile_and_assign_attrs( + code_lines=code_lines, code_scope=code_scope, show_code=show_code, + variables=variables, parameters=parameters, func_name=func_name, + sde_type=sde_type, var_type=var_type, wiener_type=wiener_type, dt=dt) def _wrap(wrapper, f, g, dt, sde_type, var_type, wiener_type, show_code): @@ -309,7 +314,7 @@ def _wrap(wrapper, f, g, dt, sde_type, var_type, wiener_type, show_code): 'scalar Wiener Process.' show_code = False if show_code is None else show_code - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt if f is not None and g is not None: return wrapper(f=f, g=g, dt=dt, show_code=show_code, sde_type=sde_type, diff --git a/brainpy/integrators/sde/srk_strong.py b/brainpy/integrators/sde/srk_strong.py index 7b1aa153..657a54dd 100644 --- a/brainpy/integrators/sde/srk_strong.py +++ b/brainpy/integrators/sde/srk_strong.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from brainpy import backend -from brainpy import profile from brainpy.integrators import constants from . import common @@ -359,7 +358,7 @@ def _srk1_wrapper(f, g, dt, sde_type, var_type, wiener_type, show_code, num_iter f'supports {constants.SUPPORTED_VAR_TYPE}') # return and compile - common.return_and_compile(code_lines, code_scope, show_code, variables) + common.return_compile_and_assign_attrs(code_lines, code_scope, show_code, variables) return code_scope[func_name] @@ -409,7 +408,7 @@ def _wrap(wrapper, f, g, dt, sde_type, var_type, wiener_type, show_code, num_ite f'But we got {wiener_type}.' show_code = False if show_code is None else show_code - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt num_iter = 10 if num_iter is None else num_iter if f is not None and g is not None: diff --git a/brainpy/integrators/sympy_analysis.py b/brainpy/integrators/sympy_analysis.py index ff8461ff..ac8d693a 100644 --- a/brainpy/integrators/sympy_analysis.py +++ b/brainpy/integrators/sympy_analysis.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- import ast -import inspect import math from collections import Counter import numpy as np from brainpy import errors -from brainpy import profile from brainpy import tools try: @@ -26,22 +24,10 @@ from sympy.codegen import cfunctions from sympy.printing.precedence import precedence from sympy.printing.str import StrPrinter - CONSTANT_NOISE = 'CONSTANT' FUNCTIONAL_NOISE = 'FUNCTIONAL' -ODE_TYPE = 'ODE' -SDE_TYPE = 'SDE' - -DIFF_EQUATION = 'diff_equation' -SUB_EXPRESSION = 'sub_expression' - - FUNCTION_MAPPING = { - # 'real': sympy.functions.elementary.complexes.re, - # 'imag': sympy.functions.elementary.complexes.im, - # 'conjugate': sympy.functions.elementary.complexes.conjugate, - # functions in inherit python # --------------------------- 'abs': sympy.functions.elementary.complexes.Abs, @@ -64,9 +50,6 @@ FUNCTION_MAPPING = { 'expm1': cfunctions.expm1, 'exp2': cfunctions.exp2, - # 'maximum': sympy.functions.elementary.miscellaneous.Max, - # 'minimum': sympy.functions.elementary.miscellaneous.Min, - # functions in math # ------------------ 'asin': sympy.functions.elementary.trigonometric.asin, @@ -105,65 +88,42 @@ CONSTANT_MAPPING = { 'inf': sympy.S.Infinity, } +# Get functions in math +_functions_in_math = [] +for key in dir(math): + if not key.startswith('__'): + _functions_in_math.append(getattr(math, key)) + +# Get functions in NumPy +_functions_in_numpy = [] +for key in dir(np): + if not key.startswith('__'): + _functions_in_numpy.append(getattr(np, key)) +for key in dir(np.random): + if not key.startswith('__'): + _functions_in_numpy.append(getattr(np.random, key)) +for key in dir(np.linalg): + if not key.startswith('__'): + _functions_in_numpy.append(getattr(np.linalg, key)) + + +def func_in_numpy_or_math(func): + return func in _functions_in_math or func in _functions_in_numpy + def get_mapping_scope(): - if profile.run_on_cpu(): - return { - 'sign': np.sign, 'cos': np.cos, 'sin': np.sin, 'tan': np.tan, - 'sinc': np.sinc, 'arcsin': np.arcsin, 'arccos': np.arccos, - 'arctan': np.arctan, 'arctan2': np.arctan2, 'cosh': np.cosh, - 'sinh': np.cosh, 'tanh': np.tanh, 'arcsinh': np.arcsinh, - 'arccosh': np.arccosh, 'arctanh': np.arctanh, 'ceil': np.ceil, - 'floor': np.floor, 'log': np.log, 'log2': np.log2, 'log1p': np.log1p, - 'log10': np.log10, 'exp': np.exp, 'expm1': np.expm1, 'exp2': np.exp2, - 'hypot': np.hypot, 'sqrt': np.sqrt, 'pi': np.pi, 'e': np.e, 'inf': np.inf, - 'asin': math.asin, 'acos': math.acos, 'atan': math.atan, 'atan2': math.atan2, - 'asinh': math.asinh, 'acosh': math.acosh, 'atanh': math.atanh, - # 'Max': np.maximum, 'Min': np.minimum - } - else: - return { - # functions in numpy - # ------------------ - 'arcsin': math.asin, 'arccos': math.acos, - 'arctan': math.atan, 'arctan2': math.atan2, 'arcsinh': math.asinh, - 'arccosh': math.acosh, 'arctanh': math.atanh, - 'sign': np.sign, 'sinc': np.sinc, - 'log2': np.log2, 'log1p': np.log1p, - 'expm1': np.expm1, 'exp2': np.exp2, - # 'Max': max, 'Min': min, - - # functions in math - # ------------------ - 'asin': math.asin, - 'acos': math.acos, - 'atan': math.atan, - 'atan2': math.atan2, - 'asinh': math.asinh, - 'acosh': math.acosh, - 'atanh': math.atanh, - - # functions in both numpy and math - # -------------------------------- - 'cos': math.cos, - 'sin': math.sin, - 'tan': math.tan, - 'cosh': math.cosh, - 'sinh': math.sinh, - 'tanh': math.tanh, - 'log': math.log, - 'log10': math.log10, - 'sqrt': math.sqrt, - 'exp': math.exp, - 'hypot': math.hypot, - 'ceil': math.ceil, - 'floor': math.floor, - - # constants in both numpy and math - # -------------------------------- - 'pi': math.pi, - 'e': math.e, - 'inf': math.inf} + return { + 'sign': np.sign, 'cos': np.cos, 'sin': np.sin, 'tan': np.tan, + 'sinc': np.sinc, 'arcsin': np.arcsin, 'arccos': np.arccos, + 'arctan': np.arctan, 'arctan2': np.arctan2, 'cosh': np.cosh, + 'sinh': np.cosh, 'tanh': np.tanh, 'arcsinh': np.arcsinh, + 'arccosh': np.arccosh, 'arctanh': np.arctanh, 'ceil': np.ceil, + 'floor': np.floor, 'log': np.log, 'log2': np.log2, 'log1p': np.log1p, + 'log10': np.log10, 'exp': np.exp, 'expm1': np.expm1, 'exp2': np.exp2, + 'hypot': np.hypot, 'sqrt': np.sqrt, 'pi': np.pi, 'e': np.e, 'inf': np.inf, + 'asin': math.asin, 'acos': math.acos, 'atan': math.atan, 'atan2': math.atan2, + 'asinh': math.asinh, 'acosh': math.acosh, 'atanh': math.atanh, + } class Parser(object): @@ -479,73 +439,37 @@ class SingleDiffEq(object): Parameters ---------- - func : callable - The user defined differential equation. + var_name : str + The variable names. + variables : list + The code variables. + expressions : list + The code expressions for each line. + derivative_expr : str + The final derivative expression. + scope : dict + The code scope. """ - def __init__(self, func): - # check - if func is None: - raise errors.DiffEqError('"func" cannot be None.') - if not (callable(func) and type(func).__name__ == 'function'): - raise errors.DiffEqError('"func" must be a function.') - - # function - self.func = func - - # function string - self.code = tools.deindent(tools.get_main_code(func)) - if 'return' not in self.code: - raise errors.DiffEqError(f'"func" function must return something, ' - f'but found no return.\n{self.code}') - - # function arguments - self.func_args = inspect.getfullargspec(func).args - - # function name - if tools.is_lambda_function(func): - self.func_name = f'_integral_{self.func_args[0]}_' - else: - self.func_name = func.__name__ - + def __init__(self, var_name, variables, expressions, derivative_expr, scope, + func_name): + self.func_name = func_name # function scope - scope = inspect.getclosurevars(func) - self.func_scope = dict(scope.nonlocals) - self.func_scope.update(scope.globals) + self.func_scope = scope # differential variable name and time name - self.var_name = self.func_args[0] - self.t_name = self.func_args[1] + self.var_name = var_name + self.t_name = 't' # analyse function code - res = analyse_diff_eq(self.code) - self.expressions = [Expression(v, expr) for v, expr in zip(res.variables, res.expressions)] - self.return_type = res.return_type - self.f_expr = None - self.g_expr = None - if res.f_expr is not None: - self.f_expr = Expression(res.f_expr[0], res.f_expr[1]) - if res.g_expr is not None: - self.g_expr = Expression(res.g_expr[0], res.g_expr[1]) - for k, num in Counter(res.variables).items(): + self.expressions = [Expression(v, expr) for v, expr in zip(variables, expressions)] + self.f_expr = Expression('_f_res_', derivative_expr) + for k, num in Counter(variables).items(): if num > 1: - raise errors.DiffEqError( + raise errors.AnalyzerError( f'Found "{k}" {num} times. Please assign each expression ' f'in differential function with a unique name. ') - # analyse noise type - self.g_type = CONSTANT_NOISE - self.g_value = None - if self.g_expr is not None: - self._substitute(self.g_expr, self.expressions) - g_code = self.g_expr.get_code(subs=True) - for idf in tools.get_identifiers(g_code): - if idf not in self.func_scope: - self.g_type = FUNCTIONAL_NOISE - break - else: - self.g_value = eval(g_code, self.func_scope) - def _substitute(self, final_exp, expressions, substitute_vars=None): """Substitute expressions to get the final single expression @@ -613,7 +537,6 @@ class SingleDiffEq(object): return_expressions.append(Expression(f'_df{self.var_name}_dt', dif_eq_code)) # needed variables need_vars = tools.get_identifiers(dif_eq_code) - need_vars |= tools.get_identifiers(', '.join(self.return_intermediates)) # get the total return expressions for expr in self.expressions[::-1]: if expr.var_name in need_vars: @@ -625,30 +548,6 @@ class SingleDiffEq(object): need_vars |= tools.get_identifiers(code) return return_expressions[::-1] - def get_g_expressions(self): - if self.g_expr is None: - return [] - - if self.is_functional_noise: - return_expressions = [] - # the derivative expression - eq_code = self.g_expr.get_code(subs=True) - return_expressions.append(Expression(f'_dg{self.var_name}_dt', eq_code)) - # needed variables - need_vars = tools.get_identifiers(eq_code) - # get the total return expressions - for expr in self.expressions[::-1]: - if expr.var_name in need_vars: - if expr.substituted_code is None: - code = expr.code - else: - code = expr.substituted_code - return_expressions.append(Expression(expr.var_name, code)) - need_vars |= tools.get_identifiers(code) - return return_expressions[::-1] - else: - return [Expression(f'_dg{self.var_name}_dt', self.g_expr.get_code(subs=True))] - def _replace_expressions(self, expressions, name, y_sub, t_sub=None): """Replace expressions of df part. @@ -712,31 +611,6 @@ class SingleDiffEq(object): y_sub=y_sub, t_sub=t_sub) - def replace_g_expressions(self, name, y_sub, t_sub=None): - if self.is_functional_noise: - return self._replace_expressions(self.get_g_expressions(), - name=name, - y_sub=y_sub, - t_sub=t_sub) - else: - return [] - - @property - def is_stochastic(self): - if self.g_expr is not None: - try: - if eval(self.g_expr.code, self.func_scope) == 0.: - return False - except Exception as e: - pass - return True - else: - return False - - @property - def is_functional_noise(self): - return self.g_type == FUNCTIONAL_NOISE - @property def expr_names(self): return [expr.var_name for expr in self.expressions] diff --git a/brainpy/integrators/utils.py b/brainpy/integrators/utils.py index 0f3a1b82..ba79d806 100644 --- a/brainpy/integrators/utils.py +++ b/brainpy/integrators/utils.py @@ -1,15 +1,36 @@ # -*- coding: utf-8 -*- import inspect +from copy import deepcopy +from brainpy import backend from brainpy import errors -from brainpy import profile __all__ = [ + 'numba_func', 'get_args', ] +def numba_func(code_scope, funcs_to_jit): + if backend.get_backend() in ['numba', 'numba-parallel']: + from brainpy.backend.runners.numba_cpu_runner import NUMBA_PROFILE + import numba as nb + + profiles = deepcopy(NUMBA_PROFILE) + profiles.pop('parallel') + if isinstance(funcs_to_jit, str): + funcs_to_jit = [funcs_to_jit] + for f in funcs_to_jit: + code_scope[f] = nb.jit(**profiles)(code_scope[f]) + + elif backend.get_backend() == 'numba-cuda': + from numba import cuda + + for f in funcs_to_jit: + code_scope[f] = cuda.jit(code_scope[f], device=True) + + def get_args(f): """Get the function arguments. @@ -71,11 +92,11 @@ def get_args(f): # 2. analyze the function arguments # 2.1 class keywords class_kw = [] - if reduced_args[0] in profile.CLASS_KEYWORDS: + if reduced_args[0] in backend.CLASS_KEYWORDS: class_kw.append(reduced_args[0]) reduced_args = reduced_args[1:] for a in reduced_args: - if a in profile.CLASS_KEYWORDS: + if a in backend.CLASS_KEYWORDS: raise errors.DiffEqError(f'Class keywords "{a}" must be defined ' f'as the first argument.') # 2.2 variable names diff --git a/brainpy/measure.py b/brainpy/measure.py index a3b4c30c..631257cb 100644 --- a/brainpy/measure.py +++ b/brainpy/measure.py @@ -2,7 +2,7 @@ import numpy as np -from brainpy import profile +from brainpy import backend try: from numba import njit @@ -214,7 +214,7 @@ def firing_rate(sp_matrix, width, window='gaussian'): rate = np.sum(sp_matrix, axis=1) # window - dt = profile.get_dt() + dt = backend.get_dt() if window == 'gaussian': width1 = 2 * width / dt width2 = int(np.around(width1)) diff --git a/brainpy/profile.py b/brainpy/profile.py deleted file mode 100644 index 27ad55bb..00000000 --- a/brainpy/profile.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The setting of the overall framework by ``profile.py`` API. -""" - -__all__ = [ - 'set_class_keywords', - - 'set_dt', - 'get_dt', - - 'set_numerical_method', - 'get_numerical_method', -] - -_dt = 0.1 -_method = 'euler' -CLASS_KEYWORDS = ['self', 'cls'] - - -def set_class_keywords(*args): - global CLASS_KEYWORDS - CLASS_KEYWORDS = list(args) - - -def set_dt(dt): - """Set the numerical integrator precision. - - Parameters - ---------- - dt : float - Numerical integration precision. - """ - assert isinstance(dt, float) - global _dt - _dt = dt - - -def get_dt(): - """Get the numerical integrator precision. - - Returns - ------- - dt : float - Numerical integration precision. - """ - return _dt - - -def set_numerical_method(method): - """Set the default numerical integrator method for differential equations. - - Parameters - ---------- - method : str, callable - Numerical integrator method. - """ - from brainpy.integrators import _SUPPORT_METHODS - - if not isinstance(method, str): - raise ValueError(f'Only support string, not {type(method)}.') - if method not in _SUPPORT_METHODS: - raise ValueError(f'Unsupported numerical method: {method}.') - - global _method - _method = method - - -def get_numerical_method(): - """Get the default numerical integrator method. - - Returns - ------- - method : str - The default numerical integrator method. - """ - return _method diff --git a/brainpy/simulation/__init__.py b/brainpy/simulation/__init__.py index b4aa13e2..20fa232a 100644 --- a/brainpy/simulation/__init__.py +++ b/brainpy/simulation/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -from .population import * from .network import * +from .population import * diff --git a/brainpy/simulation/delay.py b/brainpy/simulation/delay.py new file mode 100644 index 00000000..b32c7fba --- /dev/null +++ b/brainpy/simulation/delay.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import math + +from brainpy import backend + +__all__ = [ + 'ConstantDelay', + 'push_type1', + 'push_type2', + 'pull_type0', + 'pull_type1', +] + + +class ConstantDelay(object): + """Constant delay variable for synapse computation. + + """ + + def __init__(self, size, delay_time): + self.delay_time = delay_time + self.delay_num_step = int(math.ceil(delay_time / backend.get_dt())) + 1 + self.delay_in_idx = 0 + self.delay_out_idx = self.delay_num_step - 1 + + if isinstance(size, int): + size = (size,) + size = tuple(size) + self.delay_data = backend.zeros((self.delay_num_step + 1,) + size) + + def push(self, idx_or_val, value=None): + if value is None: + self.delay_data[self.delay_in_idx] = idx_or_val + else: + self.delay_data[self.delay_in_idx][idx_or_val] = value + + def pull(self, idx=None): + if idx is None: + return self.delay_data[self.delay_out_idx] + else: + return self.delay_data[self.delay_out_idx][idx] + + def update(self): + self.delay_in_idx = (self.delay_in_idx + 1) % self.delay_num_step + self.delay_out_idx = (self.delay_out_idx + 1) % self.delay_num_step + + +def push_type1(idx_or_val, delay_data, delay_in_idx): + delay_data[delay_in_idx] = idx_or_val + + +def push_type2(idx_or_val, value, delay_data, delay_in_idx): + delay_data[delay_in_idx][idx_or_val] = value + + +def pull_type0(delay_data, delay_out_idx): + return delay_data[delay_out_idx] + + +def pull_type1(idx, delay_data, delay_out_idx): + return delay_data[delay_out_idx][idx] diff --git a/brainpy/simulation/network.py b/brainpy/simulation/network.py index b8260d86..c021197a 100644 --- a/brainpy/simulation/network.py +++ b/brainpy/simulation/network.py @@ -3,7 +3,6 @@ from collections import OrderedDict from brainpy import backend -from brainpy import profile from brainpy.simulation import population from brainpy.simulation import utils @@ -95,7 +94,7 @@ class Network(object): """ # preparation start, end = utils.check_duration(duration) - dt = profile.get_dt() + dt = backend.get_dt() ts = backend.arange(start, end, dt) # build the network @@ -112,9 +111,12 @@ class Network(object): # end self.t_start, self.t_end = start, end + for obj in self.all_nodes.values(): + if len(obj.mon['vars']) > 0: + obj.mon['ts'] = ts @property def ts(self): """Get the time points of the network. """ - return backend.arange(self.t_start, self.t_end, profile.get_dt()) + return backend.arange(self.t_start, self.t_end, backend.get_dt()) diff --git a/brainpy/simulation/population.py b/brainpy/simulation/population.py index 0e6529e9..fa9bd928 100644 --- a/brainpy/simulation/population.py +++ b/brainpy/simulation/population.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- +from collections import OrderedDict + from brainpy import backend -from brainpy import connectivity from brainpy import errors -from brainpy import profile from brainpy.simulation import constants +from brainpy.simulation import delay from brainpy.simulation import utils from brainpy.simulation.monitors import Monitor __all__ = [ 'Population', 'NeuGroup', + 'SynConn', 'TwoEndConn', ] @@ -37,8 +39,7 @@ class Population(object): target_backend = None - def __init__(self, size, steps, monitors, ensemble_type, name, - host=None, show_code=False): + def __init__(self, steps, monitors, ensemble_type, name, host=None, show_code=False): # host of the data # ---------------- if host is None: @@ -55,26 +56,14 @@ class Population(object): # model # ----- if callable(steps): - self.steps = [steps] + self.steps = OrderedDict([(steps.__name__, steps)]) elif isinstance(steps, (list, tuple)) and callable(steps[0]): - self.steps = list(steps) + self.steps = OrderedDict([(step.__name__, step) for step in steps]) + elif isinstance(steps, dict): + self.steps = steps else: raise errors.ModelDefError(f'Unknown model type: {type(steps)}. Currently, BrainPy ' - f'only supports: function, list of functions.') - - # size - # ---- - if isinstance(size, (list, tuple)): - if len(size) <= 0: - raise errors.ModelDefError('size must be int, or a tuple/list of int.') - if not isinstance(size[0], int): - raise errors.ModelDefError('size must be int, or a tuple/list of int.') - size = tuple(size) - elif isinstance(size, int): - size = (size,) - else: - raise errors.ModelDefError('size must be int, or a tuple/list of int.') - self.size = size + f'only supports: function, list/tuple/dict of functions.') # name # ---- @@ -109,12 +98,12 @@ class Population(object): self.target_backend = [self.target_backend] assert isinstance(self.target_backend, (tuple, list)), 'target_backend must be a list/tuple.' - def build(self, format_inputs, return_code=True, mon_length=0): + def build(self, inputs, input_is_formatted=False, return_code=True, mon_length=0, show_code=False): """Build the object for running. Parameters ---------- - format_inputs : list, tuple, optional + inputs : list, tuple, optional The object inputs. return_code : bool Whether return the formatted codes. @@ -126,14 +115,16 @@ class Population(object): calls : list, tuple The code lines to call step functions. """ - if backend.get_backend() not in self.target_backend: + if (self.target_backend[0] != 'general') and (backend.get_backend() not in self.target_backend): raise errors.ModelDefError(f'The model {self.name} is target to run on {self.target_backend},' f'but currently the default backend of BrainPy is ' - f'{profile.get_backend()}') - return self.runner.build(formatted_inputs=format_inputs, + f'{backend.get_backend()}') + if not input_is_formatted: + inputs = utils.format_pop_level_inputs(inputs, self, mon_length) + return self.runner.build(formatted_inputs=inputs, mon_length=mon_length, return_code=return_code, - show_code=self.show_code) + show_code=(self.show_code or show_code)) def run(self, duration, inputs=(), report=False, report_percent=0.1): """The running function. @@ -153,13 +144,12 @@ class Population(object): # times # ------ start, end = utils.check_duration(duration) - times = backend.arange(start, end, profile.get_dt()) + times = backend.arange(start, end, backend.get_dt()) run_length = backend.shape(times)[0] # build run function # ------------------ - format_inputs = utils.format_pop_level_inputs(inputs, self, run_length, self.size) - self.run_func = self.build(format_inputs, mon_length=run_length, return_code=False) + self.run_func = self.build(inputs, input_is_formatted=False, mon_length=run_length, return_code=False) # run the model # ------------- @@ -206,28 +196,77 @@ class NeuGroup(Population): The name of the neuron group. """ - def __init__(self, size, steps, monitors=None, name=None, - host=None, show_code=False): + def __init__(self, steps, size, monitors=None, name=None, + host=None, show_code=False, ensemble_type=None): # name # ----- if name is None: - name = 'NeuGroup' + name = '' + else: + name = '_' + name global _NeuGroup_NO _NeuGroup_NO += 1 - name = f'NG{_NeuGroup_NO}_{name}' + name = f'NG{_NeuGroup_NO}{name}' + + # size + # ---- + if isinstance(size, (list, tuple)): + if len(size) <= 0: + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + if not isinstance(size[0], int): + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + size = tuple(size) + elif isinstance(size, int): + size = (size,) + else: + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + self.size = size # initialize # ---------- - super(NeuGroup, self).__init__(size=size, - steps=steps, + ensemble_type = constants.NEU_GROUP_TYPE if ensemble_type is None else ensemble_type + super(NeuGroup, self).__init__(steps=steps, monitors=monitors, name=name, host=host, - ensemble_type=constants.NEU_GROUP_TYPE, + ensemble_type=ensemble_type, show_code=show_code) -class TwoEndConn(Population): +class SynConn(Population): + """Synaptic Connections. + """ + + def __init__(self, steps, monitors, ensemble_type, name, host=None, show_code=False): + # check delay update + if callable(steps): + steps = OrderedDict([(steps.__name__, steps)]) + elif isinstance(steps, (tuple, list)) and callable(steps[0]): + steps = OrderedDict([(step.__name__, step) for step in steps]) + else: + assert isinstance(steps, dict) + if hasattr(self, 'constant_delays'): + for key, delay_var in self.constant_delays.items(): + if delay_var.update not in steps: + delay_name = f'{key}_delay_update' + setattr(self, delay_name, delay_var.update) + steps[delay_name] = delay_var.update + super(SynConn, self).__init__(steps=steps, monitors=monitors, ensemble_type=ensemble_type, + name=name, host=host, show_code=show_code) + + for key, delay_var in self.constant_delays.items(): + delay_var.name = f'{self.name}_delay_{key}' + + def register_constant_delay(self, key, size, delay_time): + if not hasattr(self, 'constant_delays'): + self.constant_delays = {} + if key in self.constant_delays: + raise errors.ModelDefError(f'"{key}" has been registered as an constant delay.') + self.constant_delays[key] = delay.ConstantDelay(size, delay_time) + return self.constant_delays[key] + + +class TwoEndConn(SynConn): """Two End Synaptic Connections. Parameters @@ -238,48 +277,39 @@ class TwoEndConn(Population): Pre-synaptic neuron group. post : neurons.NeuGroup, neurons.NeuSubGroup Post-synaptic neuron group. - conn : connectivity.Connector - Connection method to create synaptic connectivity. monitors : list, tuple Variables to monitor. name : str The name of the neuron group. """ - def __init__(self, steps, pre=None, post=None, conn=None, monitors=None, - name=None, host=None, show_code=False): + def __init__(self, steps, pre, post, monitors=None, name=None, + host=None, show_code=False, ensemble_type=None): # name # ---- if name is None: - name = 'TwoEndConn' + name = '' + else: + name = '_' + name global _TwoEndSyn_NO _TwoEndSyn_NO += 1 - name = f'TEC{_TwoEndSyn_NO}_{name}' + name = f'TEC{_TwoEndSyn_NO}{name}' # pre or post neuron group # ------------------------ + if not isinstance(pre, NeuGroup): + raise errors.ModelUseError('"pre" must be an instance of NeuGroup.') self.pre = pre + if not isinstance(post, NeuGroup): + raise errors.ModelUseError('"post" must be an instance of NeuGroup.') self.post = post - self.conn = None - if pre is not None and post is not None: - if not isinstance(pre, NeuGroup): - raise errors.ModelUseError('"pre" must be an instance of NeuGroup.') - if not isinstance(post, NeuGroup): - raise errors.ModelUseError('"post" must be an instance of NeuGroup.') - - if conn is not None: - if isinstance(conn, connectivity.Connector): - self.conn = conn(pre.size, post.size) - self.conn = connectivity.Connector() - - size = 1 # TODO # initialize # ---------- + ensemble_type = constants.TWO_END_TYPE if ensemble_type is None else ensemble_type super(TwoEndConn, self).__init__(steps=steps, name=name, - size=size, monitors=monitors, - ensemble_type=constants.SYN_CONN_TYPE, + ensemble_type=ensemble_type, host=host, show_code=show_code) diff --git a/brainpy/simulation/runner.py b/brainpy/simulation/runner.py index ec37eecc..33e2a856 100644 --- a/brainpy/simulation/runner.py +++ b/brainpy/simulation/runner.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- import abc -from brainpy import errors +from brainpy import errors __all__ = [ 'AbstractRunner', @@ -26,10 +26,8 @@ class NodeRunner(AbstractRunner): """ def __init__(self, host, steps): self.host = host - assert isinstance(steps, (list, tuple)) and callable(steps[0]) self.steps = steps - self.step_names = [step.__name__ for step in steps] - self.schedule = ['input'] + self.step_names + ['monitor'] + self.schedule = ['input'] + list(self.steps.keys()) + ['monitor'] def get_schedule(self): return self.schedule @@ -37,10 +35,10 @@ class NodeRunner(AbstractRunner): def set_schedule(self, schedule): if not isinstance(schedule, (list, tuple)): raise errors.ModelUseError('"schedule" must be a list/tuple.') - all_func_names = ['input', 'monitor'] + self.step_names + all_func_names = ['input', 'monitor'] + list(self.steps.keys()) for s in schedule: if s not in all_func_names: - raise errors.ModelUseError(f'Unknown step function "{s}" for model "{self.state}".') + raise errors.ModelUseError(f'Unknown step function "{s}" for model "{self.host}".') self.schedule = schedule @abc.abstractmethod diff --git a/brainpy/simulation/utils.py b/brainpy/simulation/utils.py index 1f88c4d8..e96d8881 100644 --- a/brainpy/simulation/utils.py +++ b/brainpy/simulation/utils.py @@ -4,10 +4,8 @@ import time from brainpy import backend from brainpy import errors -from brainpy import profile from brainpy.simulation import constants - __all__ = [ 'check_duration', 'run_model', @@ -64,17 +62,17 @@ def run_model(run_func, times, report, report_percent): The percent of the total running length for each report. """ run_length = len(times) - dt = profile.get_dt() + dt = backend.get_dt() if report: t0 = time.time() - for i, t in enumerate(times[:2]): + for i, t in enumerate(times[:1]): run_func(_t=t, _i=i, _dt=dt) print('Compilation used {:.4f} s.'.format(time.time() - t0)) print("Start running ...") report_gap = int(run_length * report_percent) t0 = time.time() - for run_idx in range(2, run_length): + for run_idx in range(1, run_length): run_func(_t=times[run_idx], _i=run_idx, _dt=dt) if (run_idx + 1) % report_gap == 0: percent = (run_idx + 1) / run_length * 100 @@ -86,7 +84,7 @@ def run_model(run_func, times, report, report_percent): run_func(_t=times[run_idx], _i=run_idx, _dt=dt) -def format_pop_level_inputs(inputs, host, mon_length, size): +def format_pop_level_inputs(inputs, host, mon_length): """Format the inputs of a population. Parameters @@ -97,8 +95,6 @@ def format_pop_level_inputs(inputs, host, mon_length, size): The host which contains all data. mon_length : int The monitor length. - size : tuple - The size of the population. Returns ------- @@ -143,12 +139,8 @@ def format_pop_level_inputs(inputs, host, mon_length, size): shape = backend.shape(input[1]) if shape[0] == mon_length: data_type = 'iter' - elif shape == size: - data_type = 'fix' else: - raise errors.ModelUseError(f'Unknown size of input for "{key}", ' - f'it should either be {size}, nor be ' - f'the shape of {(mon_length, ) + size}') + data_type = 'fix' # operation if len(input) == 3: @@ -230,12 +222,8 @@ def format_net_level_inputs(inputs, run_length): shape = backend.shape(val) if shape[0] == run_length: data_type = 'iter' - elif shape == target.size: - data_type = 'fix' else: - raise errors.ModelUseError(f'Unknown size of input for "{key}", it should ' - f'either be {target.size}, nor be the shape ' - f'of {(run_length,) + target.size}') + data_type = 'fix' # operation if len(input) == 4: diff --git a/brainpy/tools/codes.py b/brainpy/tools/codes.py index af679f3f..7c1c8b2c 100644 --- a/brainpy/tools/codes.py +++ b/brainpy/tools/codes.py @@ -1,12 +1,9 @@ # -*- coding: utf-8 -*- -import ast +import inspect import re from types import LambdaType -from brainpy import errors -from .ast2code import ast2code - __all__ = [ # tools for code string 'get_identifiers', @@ -15,10 +12,9 @@ __all__ = [ 'word_replace', # other tools - 'NoiseHandler', - 'FindAtomicOp', - 'find_atomic_op', 'is_lambda_function', + 'get_main_code', + 'get_func_source', ] @@ -143,91 +139,52 @@ def is_lambda_function(func): return isinstance(func, LambdaType) and func.__name__ == "" -class NoiseHandler(object): - normal_pattern = re.compile(r'(_normal_like_)\((\w+)\)') - - @staticmethod - def vector_replace_f(m): - return 'numpy.random.normal(0., 1., ' + m.group(2) + '.shape)' - - @staticmethod - def scalar_replace_f(m): - return 'numpy.random.normal(0., 1.)' - - @staticmethod - def cuda_replace_f(m): - return 'xoroshiro128p_normal_float64(rng_states, _obj_i)' - - -class FindAtomicOp(ast.NodeTransformer): - def __init__(self, var2idx): - self.var2idx = var2idx - self.left = None - self.right = None - - def visit_Assign(self, node): - targets = node.targets - try: - assert len(targets) == 1 - except AssertionError: - raise errors.DiffEqError('Do not support multiple assignment.') - left = ast2code(ast.fix_missing_locations(targets[0])) - key = targets[0].slice.value.s - value = targets[0].value.id - if node.value.__class__.__name__ == 'BinOp': - r_left = ast2code(ast.fix_missing_locations(node.value.left)) - r_right = ast2code(ast.fix_missing_locations(node.value.right)) - op = ast2code(ast.fix_missing_locations(node.value.op)) - if op not in ['+', '-']: - # raise ValueError(f'Unsupported operation "{op}" for {left}.') - return node - self.left = f'{value}[{self.var2idx[key]}]' - if r_left == left: - if op == '+': - self.right = r_right - if op == '-': - self.right = f'- {r_right}' - elif r_left == '-' + left: - if op == '+': - self.right = f"2 * {left} + {r_right}" - if op == '-': - self.right = f"2 * {left} - {r_right}" - elif r_right == left: - if op == '+': - self.right = r_left - if op == '-': - self.right = f"{r_left} + 2 * {left}" - elif r_right == '-' + left: - if op == '+': - self.right = f"{r_left} + 2 * {left}" - if op == '-': - self.right = r_left +def get_func_source(func): + code = inspect.getsource(func) + # remove @ + try: + start = code.index('def ') + code = code[start:] + except ValueError: + pass + return code + + +def get_main_code(func): + """Get the main function _code string. + + For lambda function, return the + + Parameters + ---------- + func : callable, Optional, int, float + + Returns + ------- + + """ + if func is None: + return '' + elif callable(func): + if is_lambda_function(func): + func_code = get_func_source(func) + splits = func_code.split(':') + if len(splits) != 2: + raise ValueError(f'Can not parse function: \n{func_code}') + return f'return {splits[1]}' + + else: + func_codes = inspect.getsourcelines(func)[0] + idx = 0 + for i, line in enumerate(func_codes): + idx += 1 + line = line.replace(' ', '') + if '):' in line: + break else: - return node - return node - - def visit_AugAssign(self, node): - op = ast2code(ast.fix_missing_locations(node.op)) - expr = ast2code(ast.fix_missing_locations(node.value)) - if op not in ['+', '-']: - # left = ast2code(ast.fix_missing_locations(node.target)) - # raise ValueError(f'Unsupported operation "{op}" for {left}.') - return node - - key = node.target.slice.value.s - value = node.target.value.id - - self.left = f'{value}[{self.var2idx[key]}]' - if op == '+': - self.right = expr - if op == '-': - self.right = f'- {expr}' - - return node - - -def find_atomic_op(code_line, var2idx): - tree = ast.parse(code_line.strip()) - formatter = FindAtomicOp(var2idx) - formatter.visit(tree) - return formatter + code = "\n".join(func_codes) + raise ValueError(f'Can not parse function: \n{code}') + return ''.join(func_codes[idx:]) + else: + raise ValueError(f'Unknown function type: {type(func)}.') + diff --git a/brainpy/visualization/figures.py b/brainpy/visualization/figures.py index 98c29fb9..7f01ab94 100644 --- a/brainpy/visualization/figures.py +++ b/brainpy/visualization/figures.py @@ -9,18 +9,18 @@ __all__ = [ ] -def get_figure(n_row, n_col, len_row=3, len_col=6): +def get_figure(row_num, col_num, row_len=3, col_len=6): """Get the constrained_layout figure. Parameters ---------- - n_row : int + row_num : int The row number of the figure. - n_col : int + col_num : int The column number of the figure. - len_row : int, float + row_len : int, float The length of each row. - len_col : int, float + col_len : int, float The length of each column. Returns @@ -28,6 +28,6 @@ def get_figure(n_row, n_col, len_row=3, len_col=6): fig_and_gs : tuple Figure and GridSpec. """ - fig = plt.figure(figsize=(n_col * len_col, n_row * len_row), constrained_layout=True) - gs = GridSpec(n_row, n_col, figure=fig) + fig = plt.figure(figsize=(col_num * col_len, row_num * row_len), constrained_layout=True) + gs = GridSpec(row_num, col_num, figure=fig) return fig, gs diff --git a/brainpy/visualization/plots.py b/brainpy/visualization/plots.py index f7db68a4..43467080 100644 --- a/brainpy/visualization/plots.py +++ b/brainpy/visualization/plots.py @@ -5,7 +5,7 @@ import numpy as np from matplotlib import animation from matplotlib.gridspec import GridSpec -from brainpy import profile +from brainpy import backend from brainpy.errors import ModelUseError __all__ = [ @@ -234,7 +234,7 @@ def animate_2D(values, figure : plt.figure The created figure instance. """ - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt num_step, num_neuron = values.shape height, width = net_size val_min = values.min() if val_min is None else val_min @@ -336,7 +336,7 @@ def animate_1D(dynamical_vars, """ # check dt - dt = profile.get_dt() if dt is None else dt + dt = backend.get_dt() if dt is None else dt # check figure fig = plt.figure(figsize=(figsize or (6, 6)), constrained_layout=True) diff --git a/examples/FitzHugh_Nagumo.py b/examples/FitzHugh_Nagumo.py deleted file mode 100644 index 1ceb5f3f..00000000 --- a/examples/FitzHugh_Nagumo.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np - -import brainpy as bp - -bp.backend.set('numpy') - - -class FitzHughNagumo(bp.NeuGroup): - - def __init__(self, size, a=0.7, b=0.8, tau=12.5, Vth=1.9, monitors=None): - self.a = a - self.b = b - self.tau = tau - self.Vth = Vth - - @bp.odeint(method='rk4') - def integral(v, w, t, Iext): - dw = (v + a - b * w) / tau - dv = v - v * v * v / 3 - w + Iext - return dv, dw - - self.integral = integral - - self.V = np.zeros(size) - self.w = np.zeros(size) - self.spike = np.zeros(size) - self.input = np.zeros(size) - - super(FitzHughNagumo, self).__init__( - size=size, - steps=[self.update], - monitors=monitors, - name='FN_model', - show_code=True, - target_backend=['numpy'], - ) - - def update(self, _t): - v, self.w = self.integral(self.V, self.w, _t, self.input) - self.spike = np.logical_and(v >= self.Vth, self.V < self.Vth) - self.V = v - self.input = 0. - - -neurons = FitzHughNagumo(100, monitors=['V']) -neurons.run(300., inputs=('input', 1.), report=True) -bp.visualize.line_plot(neurons.mon.ts, neurons.mon.V, show=True) diff --git a/examples/hh_numba_cpu.py b/examples/hh_numba_cpu.py deleted file mode 100644 index cc0410e7..00000000 --- a/examples/hh_numba_cpu.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- - - -import numba as nb -import numpy as np - -import brainpy as bp - -bp.backend.set('numba') -bp.profile.set(dt=0.02) - - -class HH(bp.NeuGroup): - target_backend = ['numpy', 'numba'] - - def __init__(self, size, monitors, E_Na=50., E_K=-77., E_leak=-54.387, - C=1.0, g_Na=120., g_K=36., g_leak=0.03, V_th=20.): - self.E_Na = E_Na - self.E_K = E_K - self.E_leak = E_leak - self.C = C - self.g_Na = g_Na - self.g_K = g_K - self.g_leak = g_leak - self.V_th = V_th - - @nb.njit - @bp.odeint(method='rk4', show_code=True) - @nb.njit - def integral(V, m, h, n, t, Iext): - alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) - beta = 4.0 * np.exp(-(V + 65) / 18) - dmdt = alpha * (1 - m) - beta * m - - alpha = 0.07 * np.exp(-(V + 65) / 20.) - beta = 1 / (1 + np.exp(-(V + 35) / 10)) - dhdt = alpha * (1 - h) - beta * h - - alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) - beta = 0.125 * np.exp(-(V + 65) / 80) - dndt = alpha * (1 - n) - beta * n - - I_Na = (g_Na * np.power(m, 3.0) * h) * (V - E_Na) - I_K = (g_K * np.power(n, 4.0)) * (V - E_K) - I_leak = g_leak * (V - E_leak) - dVdt = (- I_Na - I_K - I_leak + Iext) / C - - return dVdt, dmdt, dhdt, dndt - self.integral = integral - - self.V = np.ones(size) * -65. - self.m = np.ones(size) * 0.5 - self.h = np.ones(size) * 0.6 - self.n = np.ones(size) * 0.32 - self.spike = np.zeros(size) - self.input = np.zeros(size) - - super(HH, self).__init__( - size=size, - steps=[self.update], - monitors=monitors, - name='HH', - show_code=True, - ) - - def update(self, _t): - V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, self.input) - self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) - self.V = V - self.m = m - self.h = h - self.n = n - self.input = 0 - - -group = HH(100, monitors=['V']) -group.run(200., report=True) -bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) -group.run(200., inputs=('input', 10.), report=True) -bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) diff --git a/examples/neurons/FitzHugh_Nagumo.py b/examples/neurons/FitzHugh_Nagumo.py new file mode 100644 index 00000000..a2fd3a01 --- /dev/null +++ b/examples/neurons/FitzHugh_Nagumo.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +import brainpy as bp + +bp.backend.set('numpy', dt=0.02) + + +class FitzHughNagumo(bp.NeuGroup): + target_backend = 'general' + + def __init__(self, size, a=0.7, b=0.8, tau=12.5, Vth=1.9, **kwargs): + self.a = a + self.b = b + self.tau = tau + self.Vth = Vth + + self.V = bp.backend.zeros(size) + self.w = bp.backend.zeros(size) + self.spike = bp.backend.zeros(size) + self.input = bp.backend.zeros(size) + + super(FitzHughNagumo, self).__init__(size=size, steps=[self.update], **kwargs) + + @staticmethod + @bp.odeint(method='rk4') + def integral(V, w, t, Iext, a, b, tau): + dw = (V + a - b * w) / tau + dV = V - V * V * V / 3 - w + Iext + return dV, dw + + def update(self, _t): + V, self.w = self.integral(self.V, self.w, _t, self.input, self.a, self.b, self.tau) + self.spike = (V >= self.Vth) * (self.V < self.Vth) + self.V = V + self.input[:] = 0. + + +if __name__ == '__main__': + FNs = FitzHughNagumo(100, monitors=['V']) + + # simulation + FNs.run(300., inputs=('input', 1.), report=True) + bp.visualize.line_plot(FNs.mon.ts, FNs.mon.V, show=True) + + # phase plane analysis + phase = bp.analysis.PhasePlane(FNs.integral, + target_vars={'V': [-3, 2], 'w': [-2, 2]}, + fixed_vars=None, + pars_update={'Iext': 1., "a": 0.7, 'b': 0.8, 'tau': 12.5}) + phase.plot_nullcline() + phase.plot_fixed_point() + phase.plot_vector_field(show=True) + + # bifurcation analysis + bifurcation = bp.analysis.Bifurcation(FNs.integral, + target_pars=dict(Iext=[-1, 1], a=[0.3, 0.8]), + target_vars={'V': [-3, 2], 'w': [-2, 2]}, + fixed_vars=None, + pars_update={'b': 0.8, 'tau': 12.5}, + numerical_resolution=0.01) + bifurcation.plot_bifurcation(show=True) diff --git a/examples/neurons/HH_model.py b/examples/neurons/HH_model.py new file mode 100644 index 00000000..fe1ca116 --- /dev/null +++ b/examples/neurons/HH_model.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + + +import brainpy as bp + +bp.backend.set('pytorch', dt=0.02) + + +class HH(bp.NeuGroup): + target_backend = 'general' + + def __init__(self, size, ENa=50., EK=-77., EL=-54.387, + C=1.0, gNa=120., gK=36., gL=0.03, V_th=20., + **kwargs): + # parameters + self.ENa = ENa + self.EK = EK + self.EL = EL + self.C = C + self.gNa = gNa + self.gK = gK + self.gL = gL + self.V_th = V_th + + # variables + self.V = bp.backend.ones(size) * -65. + self.m = bp.backend.ones(size) * 0.5 + self.h = bp.backend.ones(size) * 0.6 + self.n = bp.backend.ones(size) * 0.32 + self.spike = bp.backend.zeros(size) + self.input = bp.backend.zeros(size) + + super(HH, self).__init__(size=size, steps=[self.update], **kwargs) + + @staticmethod + @bp.odeint(method='rk4', show_code=True) + def integral(V, m, h, n, t, Iext, gNa, ENa, gK, EK, gL, EL, C): + alpha = 0.1 * (V + 40) / (1 - bp.backend.exp(-(V + 40) / 10)) + beta = 4.0 * bp.backend.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * bp.backend.exp(-(V + 65) / 20.) + beta = 1 / (1 + bp.backend.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + + alpha = 0.01 * (V + 55) / (1 - bp.backend.exp(-(V + 55) / 10)) + beta = 0.125 * bp.backend.exp(-(V + 65) / 80) + dndt = alpha * (1 - n) - beta * n + + I_Na = (gNa * m ** 3.0 * h) * (V - ENa) + I_K = (gK * n ** 4.0) * (V - EK) + I_leak = gL * (V - EL) + dVdt = (- I_Na - I_K - I_leak + Iext) / C + + return dVdt, dmdt, dhdt, dndt + + def update(self, _t): + V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, + self.input, self.gNa, self.ENa, self.gK, + self.EK, self.gL, self.EL, self.C) + self.spike = (self.V < self.V_th) * (V >= self.V_th) + self.V = V + self.m = m + self.h = h + self.n = n + self.input[:] = 0 + + +if __name__ == '__main__': + group = HH(100, monitors=['V'], show_code=True) + + group.run(200., inputs=('input', 10.), report=True) + bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) + + group.run(200., report=True) + bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) diff --git a/examples/synapses/AMPA_synapse.py b/examples/synapses/AMPA_synapse.py new file mode 100644 index 00000000..20e98bfc --- /dev/null +++ b/examples/synapses/AMPA_synapse.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +import numpy as np +from numba import prange + +import brainpy as bp + +bp.integrators.set_default_odeint('rk4') +bp.backend.set(backend='numba', dt=0.01) + + +class HH(bp.NeuGroup): + target_backend = ['numpy', 'numba', 'numba-parallel'] + + def __init__(self, size, ENa=50., EK=-77., EL=-54.387, + C=1.0, gNa=120., gK=36., gL=0.03, V_th=20., + **kwargs): + # parameters + self.ENa = ENa + self.EK = EK + self.EL = EL + self.C = C + self.gNa = gNa + self.gK = gK + self.gL = gL + self.V_th = V_th + + # variables + self.V = np.ones(size) * -65. + self.m = np.ones(size) * 0.5 + self.h = np.ones(size) * 0.6 + self.n = np.ones(size) * 0.32 + self.spike = np.zeros(size) + self.input = np.zeros(size) + + super(HH, self).__init__(size=size, steps=[self.update], **kwargs) + + @staticmethod + @bp.odeint + def integral(V, m, h, n, t, Iext, gNa, ENa, gK, EK, gL, EL, C): + alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + beta = 4.0 * np.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * np.exp(-(V + 65) / 20.) + beta = 1 / (1 + np.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + + alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) + beta = 0.125 * np.exp(-(V + 65) / 80) + dndt = alpha * (1 - n) - beta * n + + I_Na = (gNa * np.power(m, 3.0) * h) * (V - ENa) + I_K = (gK * np.power(n, 4.0)) * (V - EK) + I_leak = gL * (V - EL) + dVdt = (- I_Na - I_K - I_leak + Iext) / C + + return dVdt, dmdt, dhdt, dndt + + def update(self, _t): + V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, + self.input, self.gNa, self.ENa, self.gK, + self.EK, self.gL, self.EL, self.C) + self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) + self.V = V + self.m = m + self.h = h + self.n = n + self.input[:] = 0 + + +class AMPA1_vec(bp.TwoEndConn): + target_backend = ['numpy', 'numba', 'numba-parallel', 'numa-cuda'] + + def __init__(self, pre, post, conn, delay=0., g_max=0.10, E=0., tau=2.0, **kwargs): + # parameters + self.g_max = g_max + self.E = E + self.tau = tau + self.delay = delay + + # connections + self.conn = conn(pre.size, post.size) + self.pre_ids, self.post_ids = conn.requires('pre_ids', 'post_ids') + self.size = len(self.pre_ids) + + # data + self.s = bp.backend.zeros(self.size) + self.g = self.register_constant_delay('g', size=self.size, delay_time=delay) + + super(AMPA1_vec, self).__init__(steps=[self.update, ], + pre=pre, post=post, **kwargs) + + @staticmethod + @bp.odeint(method='euler') + def int_s(s, t, tau): + return - s / tau + + def update(self, _t): + for i in prange(self.size): + pre_id = self.pre_ids[i] + self.s[i] = self.int_s(self.s[i], _t, self.tau) + self.s[i] += self.pre.spike[pre_id] + self.g.push(i, self.g_max * self.s[i]) + post_id = self.post_ids[i] + self.post.input[post_id] -= self.g.pull(i) * (self.post.V[post_id] - self.E) + + +class AMPA1_mat(bp.TwoEndConn): + target_backend = ['numpy', 'numba', 'numba-parallel'] + + def __init__(self, pre, post, conn, delay=0., g_max=0.10, E=0., tau=2.0, **kwargs): + # parameters + self.g_max = g_max + self.E = E + self.tau = tau + self.delay = delay + + # connections + self.conn = conn(pre.size, post.size) + self.conn_mat = conn.requires('conn_mat') + self.size = bp.backend.shape(self.conn_mat) + + # data + self.s = bp.backend.zeros(self.size) + self.g = self.register_constant_delay('g', size=self.size, delay_time=delay) + + super(AMPA1_mat, self).__init__(steps=[self.update, ], + pre=pre, post=post, **kwargs) + + @staticmethod + @bp.odeint + def int_s(s, t, tau): + return - s / tau + + def update(self, _t): + self.s = self.int_s(self.s, _t, self.tau) + for i in range(self.pre.size[0]): + if self.pre.spike[i] > 0: + self.s[i] += self.conn_mat[i] + self.g.push(self.g_max * self.s) + g = self.g.pull() + self.post.input -= bp.backend.sum(g, axis=0) * (self.post.V - self.E) + + +if __name__ == '__main__': + + hh = HH(100, monitors=['V']) + ampa = AMPA1_vec(pre=hh, post=hh, conn=bp.connect.All2All(), + delay=10., monitors=['s']) + net = bp.Network(hh, ampa) + net.run(100., inputs=(hh, 'input', 10.), report=True) + + fig, gs = bp.visualize.get_figure(row_num=2, col_num=1, ) + fig.add_subplot(gs[0, 0]) + bp.visualize.line_plot(hh.mon.ts, hh.mon.V) + fig.add_subplot(gs[1, 0]) + bp.visualize.line_plot(ampa.mon.ts, ampa.mon.s, show=True) diff --git a/tests/backend/runners/numba_cpu_runner.py b/tests/backend/runners/numba_cpu_runner.py new file mode 100644 index 00000000..41b47ba9 --- /dev/null +++ b/tests/backend/runners/numba_cpu_runner.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- + +import inspect +from brainpy.backend.runners.numba_cpu_runner import analyze_step_func +from brainpy.backend.runners.numba_cpu_runner import StepFuncReader + +from pprint import pprint +import ast +import numpy as np +import brainpy as bp + + +def test_analyze_step1(): + class HH(bp.NeuGroup): + target_backend = ['numpy'] + + def __init__(self, size, monitors=None, E_Na=50., E_K=-77., E_leak=-54.387, C=1.0, + g_Na=120., g_K=36., g_leak=0.03, V_th=20.): + @bp.odeint(method='rkdp', show_code=False) + def integral(V, m, h, n, t, Iext): + alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + beta = 4.0 * np.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * np.exp(-(V + 65) / 20.) + beta = 1 / (1 + np.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + + alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) + beta = 0.125 * np.exp(-(V + 65) / 80) + dndt = alpha * (1 - n) - beta * n + + I_Na = (g_Na * np.power(m, 3.0) * h) * (V - E_Na) + I_K = (g_K * np.power(n, 4.0)) * (V - E_K) + I_leak = g_leak * (V - E_leak) + dVdt = (- I_Na - I_K - I_leak + Iext) / C + + return dVdt, dmdt, dhdt, dndt + + self.E_Na = E_Na + self.E_K = E_K + self.E_leak = E_leak + self.C = C + self.g_Na = g_Na + self.g_K = g_K + self.g_leak = g_leak + self.V_th = V_th + + self.integral = integral + + self.V = np.ones(size) * -65. + self.m = np.ones(size) * 0.5 + self.h = np.ones(size) * 0.6 + self.n = np.ones(size) * 0.32 + self.spike = np.zeros(size) + self.input = np.zeros(size) + + super(HH, self).__init__(size=size, + steps=[self.update], + monitors=monitors, + name='HH') + + def update(self, _t): + V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, self.input) + self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) + self.V = V + self.m = m + self.h = h + self.n = n + self.input = 0. + + group = HH(100, ['V']) + r = analyze_step_func(group.update) + + print('Code of the function:') + print(r[0]) + print('Code Scope:') + print(r[1]) + print('Data need pass:') + print(r[2]) + print('Data need return:') + print(r[3]) + + +def test_analyze_step2(): + class HH(bp.NeuGroup): + target_backend = ['numpy'] + + def __init__(self, size, monitors=None, E_Na=50., E_K=-77., E_leak=-54.387, C=1.0, + g_Na=120., g_K=36., g_leak=0.03, V_th=20.): + @bp.odeint(method='rkdp', show_code=False) + def integral(V, m, h, n, t, Iext): + alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + beta = 4.0 * np.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * np.exp(-(V + 65) / 20.) + beta = 1 / (1 + np.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + + alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) + beta = 0.125 * np.exp(-(V + 65) / 80) + dndt = alpha * (1 - n) - beta * n + + I_Na = (g_Na * np.power(m, 3.0) * h) * (V - E_Na) + I_K = (g_K * np.power(n, 4.0)) * (V - E_K) + I_leak = g_leak * (V - E_leak) + dVdt = (- I_Na - I_K - I_leak + Iext) / C + + return dVdt, dmdt, dhdt, dndt + + self.E_Na = E_Na + self.E_K = E_K + self.E_leak = E_leak + self.C = C + self.g_Na = g_Na + self.g_K = g_K + self.g_leak = g_leak + self.V_th = V_th + + self.integral = integral + + self.V = np.ones(size) * -65. + self.m = np.ones(size) * 0.5 + self.h = np.ones(size) * 0.6 + self.n = np.ones(size) * 0.32 + self.spike = np.zeros(size) + self.input = np.zeros(size) + + super(HH, self).__init__(size=size, + steps=[self.update], + monitors=monitors, + name='HH') + + def update(self, _t): + V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, self.input) + self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) + self.V[:] = V + self.m[:] = m + self.h[:] = h + self.n[:] = n + self.input[:] = 0. + + + group = HH(100, ['V']) + r = analyze_step_func(group.update) + + print('Code of the function:') + print(r[0]) + print('Code Scope:') + print(r[1]) + print('Data need pass:') + print(r[2]) + print('Data need return:') + print(r[3]) + + +def test_StepFuncReader1(): + class HH(bp.NeuGroup): + target_backend = ['numpy', 'numba', 'numba-parallel'] + + def __init__(self, size, ENa=50., EK=-77., EL=-54.387, + C=1.0, gNa=120., gK=36., gL=0.03, V_th=20., + **kwargs): + # parameters + self.ENa = ENa + self.EK = EK + self.EL = EL + self.C = C + self.gNa = gNa + self.gK = gK + self.gL = gL + self.V_th = V_th + + # variables + self.V = np.ones(size) * -65. + self.m = np.ones(size) * 0.5 + self.h = np.ones(size) * 0.6 + self.n = np.ones(size) * 0.32 + self.spike = np.zeros(size) + self.input = np.zeros(size) + + super(HH, self).__init__(size=size, steps=[self.update], **kwargs) + + @staticmethod + @bp.odeint + def integral(V, m, h, n, t, Iext, gNa, ENa, gK, EK, gL, EL, C): + alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) + beta = 4.0 * np.exp(-(V + 65) / 18) + dmdt = alpha * (1 - m) - beta * m + + alpha = 0.07 * np.exp(-(V + 65) / 20.) + beta = 1 / (1 + np.exp(-(V + 35) / 10)) + dhdt = alpha * (1 - h) - beta * h + + alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) + beta = 0.125 * np.exp(-(V + 65) / 80) + dndt = alpha * (1 - n) - beta * n + + I_Na = (gNa * np.power(m, 3.0) * h) * (V - ENa) + I_K = (gK * np.power(n, 4.0)) * (V - EK) + I_leak = gL * (V - EL) + dVdt = (- I_Na - I_K - I_leak + Iext) / C + + return dVdt, dmdt, dhdt, dndt + + def update(self, _t): + V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, + self.input, self.gNa, self.ENa, self.gK, + self.EK, self.gL, self.EL, self.C) + self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) + self.V = V + self.m = m + self.h = h + self.n = n + self.input = 0 + + class AMPA1_vec(bp.TwoEndConn): + target_backend = ['numpy', 'numba', 'numba-parallel', 'numa-cuda'] + + def __init__(self, pre, post, conn, delay=0., g_max=0.10, E=0., tau=2.0, **kwargs): + # parameters + self.g_max = g_max + self.E = E + self.tau = tau + self.delay = delay + + # connections + self.conn = conn(pre.size, post.size) + self.pre_ids, self.post_ids = conn.requires('pre_ids', 'post_ids') + self.size = len(self.pre_ids) + + # data + self.s = bp.backend.zeros(self.size) + self.g = self.register_constant_delay('g', size=self.size, delay_time=delay) + + super(AMPA1_vec, self).__init__(steps=[self.update, ], + pre=pre, post=post, **kwargs) + + @staticmethod + @bp.odeint(method='euler') + def int_s(s, t, tau): + return - s / tau + + def update(self, _t): + for i in range(self.size): + pre_id = self.pre_ids[i] + self.s[i] = self.int_s(self.s[i], _t, self.tau) + self.s[i] += self.pre.spike[pre_id] + self.g.push(i, self.g_max * self.s[i]) + + post_id = self.post_ids[i] + self.post.input[post_id] -= self.g.pull(i) * (self.post.V[post_id] - self.E) + + hh = HH(2) + ampa = AMPA1_vec(pre=hh, post=hh, conn=bp.connect.All2All()) + + update_code = bp.tools.deindent(inspect.getsource(ampa.update)) + # output_code = bp.tools.deindent(inspect.getsource(ampa.output)) + + formatter = StepFuncReader(host=ampa) + formatter.visit(ast.parse(update_code)) + + print('lefts:') + pprint(formatter.lefts) + print() + print('rights:') + pprint(formatter.rights) + print() + print('lines:') + pprint(formatter.lines) + print() + print('delay_call:') + pprint(formatter.delay_call) + print() + + +test_StepFuncReader1() + + diff --git a/tests/backend/runners/numba_runner.py b/tests/backend/runners/numba_runner.py deleted file mode 100644 index 34dc37f0..00000000 --- a/tests/backend/runners/numba_runner.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- - -from brainpy.backend.runners.numba_cpu_runner import analyze_step_func - - -import numpy as np - -import brainpy as bp - -bp.backend.set('numpy') - - -class HH(bp.NeuGroup): - target_backend = ['numpy'] - - def __init__(self, size, monitors=None, E_Na=50., E_K=-77., E_leak=-54.387, C=1.0, - g_Na=120., g_K=36., g_leak=0.03, V_th=20.): - - @bp.odeint(method='rkdp', show_code=False) - def integral(V, m, h, n, t, Iext): - alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10)) - beta = 4.0 * np.exp(-(V + 65) / 18) - dmdt = alpha * (1 - m) - beta * m - - alpha = 0.07 * np.exp(-(V + 65) / 20.) - beta = 1 / (1 + np.exp(-(V + 35) / 10)) - dhdt = alpha * (1 - h) - beta * h - - alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10)) - beta = 0.125 * np.exp(-(V + 65) / 80) - dndt = alpha * (1 - n) - beta * n - - I_Na = (g_Na * np.power(m, 3.0) * h) * (V - E_Na) - I_K = (g_K * np.power(n, 4.0)) * (V - E_K) - I_leak = g_leak * (V - E_leak) - dVdt = (- I_Na - I_K - I_leak + Iext) / C - - return dVdt, dmdt, dhdt, dndt - - self.E_Na = E_Na - self.E_K = E_K - self.E_leak = E_leak - self.C = C - self.g_Na = g_Na - self.g_K = g_K - self.g_leak = g_leak - self.V_th = V_th - - self.integral = integral - - self.V = np.ones(size) * -65. - self.m = np.ones(size) * 0.5 - self.h = np.ones(size) * 0.6 - self.n = np.ones(size) * 0.32 - self.spike = np.zeros(size) - self.input = np.zeros(size) - - super(HH, self).__init__(size=size, - steps=[self.update], - monitors=monitors, - name='HH') - - def update(self, _t): - V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t, self.input) - # m = np.clip(m, 0., 1.) - # h = np.clip(h, 0., 1.) - # n = np.clip(n, 0., 1.) - self.spike = np.logical_and(self.V < self.V_th, V >= self.V_th) - self.V = V - self.m = m - self.h = h - self.n = n - self.input = 0. - - -def test_analyze_step(): - group = HH(100, ['V']) - r = analyze_step_func(group.update) - - print('Code of the function:') - print(r[0]) - print('Code Scope:') - print(r[1]) - print('Data need pass:') - print(r[2]) - print('Data need return:') - print(r[3]) - - -test_analyze_step() -- 2.34.1 From b6585cb5e4d350d5512fea190deab2a9816d9c81 Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Mon, 22 Mar 2021 15:52:06 +0800 Subject: [PATCH 05/15] More simple Population definition --- brainpy/__init__.py | 1 + brainpy/backend/__init__.py | 1 - brainpy/simulation/constants.py | 3 +- brainpy/simulation/population.py | 40 +++++++++++---------- examples/neurons/FitzHugh_Nagumo.py | 6 +++- examples/others/lorenz_system.py | 55 +++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 examples/others/lorenz_system.py diff --git a/brainpy/__init__.py b/brainpy/__init__.py index e073d202..c64a4f6e 100644 --- a/brainpy/__init__.py +++ b/brainpy/__init__.py @@ -22,6 +22,7 @@ from . import integrators from .integrators import ode from .integrators import sde from .integrators.integrate_wrapper import * +from .integrators.constants import * # "visualization" module from . import visualization as visualize diff --git a/brainpy/backend/__init__.py b/brainpy/backend/__init__.py index f145d50f..bc30407c 100644 --- a/brainpy/backend/__init__.py +++ b/brainpy/backend/__init__.py @@ -102,7 +102,6 @@ def set(backend, module_or_operations=None, node_runner=None, 'or a dict of operations.') - def set_class_keywords(*args): global CLASS_KEYWORDS CLASS_KEYWORDS = list(args) diff --git a/brainpy/simulation/constants.py b/brainpy/simulation/constants.py index 60af9453..c9e9c7fc 100644 --- a/brainpy/simulation/constants.py +++ b/brainpy/simulation/constants.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- +UNKNOWN_TYPE = 'unknown' # name of the neuron group NEU_GROUP_TYPE = 'NeuGroup' # name of the neuron group SYN_CONN_TYPE = 'SynConn' # name of the synapse connection TWO_END_TYPE = 'TwoEndConn' # name of the two-end synaptic connection -SUPPORTED_TYPES = [NEU_GROUP_TYPE, SYN_CONN_TYPE, TWO_END_TYPE] +SUPPORTED_TYPES = [NEU_GROUP_TYPE, SYN_CONN_TYPE, TWO_END_TYPE, UNKNOWN_TYPE] # input operations SUPPORTED_INPUT_OPS = {'-': 'sub', diff --git a/brainpy/simulation/population.py b/brainpy/simulation/population.py index fa9bd928..873dad4a 100644 --- a/brainpy/simulation/population.py +++ b/brainpy/simulation/population.py @@ -16,6 +16,7 @@ __all__ = [ 'TwoEndConn', ] +_POPULATION_NO = 0 _NeuGroup_NO = 0 _TwoEndSyn_NO = 0 @@ -27,19 +28,17 @@ class Population(object): ---------- name : str The name of the (neurons/synapses) ensemble. - size : int - The number of the neurons/synapses. - steps : function, list of function + steps : callable, list of callable The callable function, or a list of callable functions. monitors : list, tuple, None Variables to monitor. - ensemble_type : str + pop_type : str Class type. """ target_backend = None - def __init__(self, steps, monitors, ensemble_type, name, host=None, show_code=False): + def __init__(self, steps, monitors=None, pop_type=None, name=None, host=None, show_code=False): # host of the data # ---------------- if host is None: @@ -48,10 +47,12 @@ class Population(object): # ensemble type # ------------- - if ensemble_type not in constants.SUPPORTED_TYPES: - print(f'Ensemble type {ensemble_type} is not registered in BrainPy. Currently, ' + if pop_type is None: + pop_type = constants.UNKNOWN_TYPE + if pop_type not in constants.SUPPORTED_TYPES: + print(f'Ensemble type {pop_type} is not registered in BrainPy. Currently, ' f'BrainPy has recognized "{constants.SUPPORTED_TYPES}".') - self.ensemble_type = ensemble_type + self.ensemble_type = pop_type # model # ----- @@ -67,6 +68,10 @@ class Population(object): # name # ---- + if name is None: + global _POPULATION_NO + name = f'POP{_POPULATION_NO}' + _POPULATION_NO += 1 if not name.isidentifier(): raise errors.ModelUseError( f'"{name}" isn\'t a valid identifier according to Python ' @@ -75,6 +80,8 @@ class Population(object): # monitors # --------- + if monitors is None: + monitors = [] self.mon = Monitor(monitors) for var in self.mon['vars']: if not hasattr(self, var): @@ -196,8 +203,7 @@ class NeuGroup(Population): The name of the neuron group. """ - def __init__(self, steps, size, monitors=None, name=None, - host=None, show_code=False, ensemble_type=None): + def __init__(self, steps, size, monitors=None, name=None, host=None, show_code=False): # name # ----- if name is None: @@ -224,12 +230,11 @@ class NeuGroup(Population): # initialize # ---------- - ensemble_type = constants.NEU_GROUP_TYPE if ensemble_type is None else ensemble_type super(NeuGroup, self).__init__(steps=steps, monitors=monitors, name=name, host=host, - ensemble_type=ensemble_type, + pop_type=constants.NEU_GROUP_TYPE, show_code=show_code) @@ -237,7 +242,7 @@ class SynConn(Population): """Synaptic Connections. """ - def __init__(self, steps, monitors, ensemble_type, name, host=None, show_code=False): + def __init__(self, steps, **kwargs): # check delay update if callable(steps): steps = OrderedDict([(steps.__name__, steps)]) @@ -251,8 +256,7 @@ class SynConn(Population): delay_name = f'{key}_delay_update' setattr(self, delay_name, delay_var.update) steps[delay_name] = delay_var.update - super(SynConn, self).__init__(steps=steps, monitors=monitors, ensemble_type=ensemble_type, - name=name, host=host, show_code=show_code) + super(SynConn, self).__init__(steps=steps, **kwargs) for key, delay_var in self.constant_delays.items(): delay_var.name = f'{self.name}_delay_{key}' @@ -283,8 +287,7 @@ class TwoEndConn(SynConn): The name of the neuron group. """ - def __init__(self, steps, pre, post, monitors=None, name=None, - host=None, show_code=False, ensemble_type=None): + def __init__(self, steps, pre, post, monitors=None, name=None, host=None, show_code=False): # name # ---- if name is None: @@ -306,10 +309,9 @@ class TwoEndConn(SynConn): # initialize # ---------- - ensemble_type = constants.TWO_END_TYPE if ensemble_type is None else ensemble_type super(TwoEndConn, self).__init__(steps=steps, name=name, monitors=monitors, - ensemble_type=ensemble_type, + pop_type=constants.TWO_END_TYPE, host=host, show_code=show_code) diff --git a/examples/neurons/FitzHugh_Nagumo.py b/examples/neurons/FitzHugh_Nagumo.py index a2fd3a01..2c3bac16 100644 --- a/examples/neurons/FitzHugh_Nagumo.py +++ b/examples/neurons/FitzHugh_Nagumo.py @@ -19,7 +19,8 @@ class FitzHughNagumo(bp.NeuGroup): self.spike = bp.backend.zeros(size) self.input = bp.backend.zeros(size) - super(FitzHughNagumo, self).__init__(size=size, steps=[self.update], **kwargs) + super(FitzHughNagumo, self).__init__( + size=size, steps=[self.update], **kwargs) @staticmethod @bp.odeint(method='rk4') @@ -42,6 +43,9 @@ if __name__ == '__main__': FNs.run(300., inputs=('input', 1.), report=True) bp.visualize.line_plot(FNs.mon.ts, FNs.mon.V, show=True) + FNs.run(300., inputs=('input', 0.6), report=True) + bp.visualize.line_plot(FNs.mon.ts, FNs.mon.V, show=True) + # phase plane analysis phase = bp.analysis.PhasePlane(FNs.integral, target_vars={'V': [-3, 2], 'w': [-2, 2]}, diff --git a/examples/others/lorenz_system.py b/examples/others/lorenz_system.py new file mode 100644 index 00000000..98bdaf32 --- /dev/null +++ b/examples/others/lorenz_system.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D + +import brainpy as bp + +bp.backend.set('numpy', dt=0.005) + + +class LorenzSystem(bp.Population): + target_backend = 'general' + + def __init__(self, size=0, sigma=10, beta=8 / 3, rho=28, p=0.1, **kwargs): + self.sigma = sigma + self.beta = beta + self.rho = rho + self.p = p + + self.x = bp.backend.ones(size) + self.y = bp.backend.ones(size) + self.z = bp.backend.ones(size) + + def lorenz_g(x, y, z, t, sigma, rho, beta, p): + return p * x, p * y, p * z + + @bp.sdeint(g=lorenz_g, sde_type=bp.ITO_SDE, wiener_type=bp.SCALAR_WIENER) + def lorenz_f(x, y, z, t, sigma, rho, beta, p): + dx = sigma * (y - x) + dy = x * (rho - z) - y + dz = x * y - beta * z + return dx, dy, dz + + self.lorenz = lorenz_f + + super(LorenzSystem, self).__init__(steps=[self.update], **kwargs) + + def update(self, _t): + self.x, self.y, self.z = self.lorenz(self.x, self.y, self.z, _t, + self.sigma, self.rho, self.beta, self.p) + + +sys = LorenzSystem(1, monitors=['x', 'y', 'z']) +sys.run(100.) + +fig = plt.figure() +ax = fig.gca(projection='3d') +plt.plot(sys.mon.x[:, 0], sys.mon.y[:, 0], sys.mon.z[:, 0]) +ax.set_xlabel('x') +ax.set_xlabel('y') +ax.set_xlabel('z') +plt.show() + + +if __name__ == '__main__': + Axes3D -- 2.34.1 From 0579156fb991d798d3a0aaa2bd0adf9383e28bf3 Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Mon, 22 Mar 2021 21:36:15 +0800 Subject: [PATCH 06/15] Separate DynamicSystem and Brain Objects --- brainpy/__init__.py | 4 +- brainpy/backend/__init__.py | 16 +- brainpy/backend/operators/bk_numba_cpu.py | 2 - brainpy/backend/operators/standard.py | 280 ++++++++++++++++ brainpy/backend/runners/numba_cuda_runner.py | 2 +- .../integrators/ode/rk_adaptive_methods.py | 6 - brainpy/integrators/ode/rk_methods.py | 5 - brainpy/integrators/ode/wrapper.py | 1 - brainpy/integrators/sde/__init__.py | 2 +- brainpy/simulation/__init__.py | 4 +- brainpy/simulation/brain_objects.py | 266 +++++++++++++++ brainpy/simulation/dynamic_system.py | 175 ++++++++++ brainpy/simulation/network.py | 122 ------- brainpy/simulation/population.py | 317 ------------------ brainpy/simulation/utils.py | 6 +- docs/Makefile | 2 +- examples/neurons/FitzHugh_Nagumo.py | 3 +- examples/neurons/HH_model.py | 2 +- examples/others/lorenz_system.py | 7 +- examples/synapses/AMPA_synapse.py | 9 +- 20 files changed, 747 insertions(+), 484 deletions(-) create mode 100644 brainpy/simulation/brain_objects.py create mode 100644 brainpy/simulation/dynamic_system.py delete mode 100644 brainpy/simulation/network.py delete mode 100644 brainpy/simulation/population.py diff --git a/brainpy/__init__.py b/brainpy/__init__.py index c64a4f6e..7f909b5c 100644 --- a/brainpy/__init__.py +++ b/brainpy/__init__.py @@ -14,8 +14,8 @@ from . import connectivity as connect # "simulation" module from . import simulation -from .simulation.population import * -from .simulation.network import * +from .simulation.dynamic_system import * +from .simulation.brain_objects import * # "integrators" module from . import integrators diff --git a/brainpy/backend/__init__.py b/brainpy/backend/__init__.py index bc30407c..34540d59 100644 --- a/brainpy/backend/__init__.py +++ b/brainpy/backend/__init__.py @@ -4,6 +4,7 @@ from types import ModuleType from brainpy import errors from .operators.bk_numpy import * +from .runners.general_runner import GeneralNodeRunner, GeneralNetRunner _backend = 'numpy' # default backend is NumPy _node_runner = None @@ -14,22 +15,23 @@ CLASS_KEYWORDS = ['self', 'cls'] NEEDED_OPS = ['as_tensor', 'normal', 'reshape', 'shape', 'exp', 'sum', 'zeros', 'ones', 'eye', 'matmul', 'vstack', 'arange'] -SUPPORTED_BACKEND = ['numba', 'numba-parallel', 'numba-cuda', 'jax', - 'numpy', 'pytorch', 'tensorflow', ] +SUPPORTED_BACKEND = { + 'numba', 'numba-parallel', 'numba-cuda', 'jax', # JIT framework + 'numpy', 'pytorch', 'tensorflow', +} SYSTEM_KEYWORDS = ['_dt', '_t', '_i'] -def set(backend, module_or_operations=None, node_runner=None, - net_runner=None, dt=None): +def set(backend, module_or_operations=None, node_runner=None, net_runner=None, dt=None): if dt is not None: set_dt(dt) if _backend == backend: return + global_vars = globals() if backend == 'numpy': from .operators import bk_numpy - from .runners.general_runner import GeneralNodeRunner, GeneralNetRunner node_runner = GeneralNodeRunner if node_runner is None else node_runner net_runner = GeneralNetRunner if net_runner is None else net_runner @@ -37,7 +39,6 @@ def set(backend, module_or_operations=None, node_runner=None, elif backend == 'pytorch': from .operators import bk_pytorch - from .runners.general_runner import GeneralNodeRunner, GeneralNetRunner node_runner = GeneralNodeRunner if node_runner is None else node_runner net_runner = GeneralNetRunner if net_runner is None else net_runner @@ -45,7 +46,6 @@ def set(backend, module_or_operations=None, node_runner=None, elif backend == 'tensorflow': from .operators import bk_tensorflow - from .runners.general_runner import GeneralNodeRunner, GeneralNetRunner node_runner = GeneralNodeRunner if node_runner is None else node_runner net_runner = GeneralNetRunner if net_runner is None else net_runner @@ -86,10 +86,8 @@ def set(backend, module_or_operations=None, node_runner=None, raise errors.ModelUseError(f'Backend "{backend}" is unknown, ' f'please provide the "module_or_operations" ' f'to specify the necessary computation units.') - from .runners.general_runner import GeneralNodeRunner node_runner = GeneralNodeRunner if node_runner is None else node_runner - global_vars = globals() global_vars['_backend'] = backend global_vars['_node_runner'] = node_runner global_vars['_net_runner'] = net_runner diff --git a/brainpy/backend/operators/bk_numba_cpu.py b/brainpy/backend/operators/bk_numba_cpu.py index f6b36f2a..f84a3c44 100644 --- a/brainpy/backend/operators/bk_numba_cpu.py +++ b/brainpy/backend/operators/bk_numba_cpu.py @@ -12,7 +12,6 @@ sum = np.sum zeros = np.zeros ones = np.ones eye = np.eye -outer = np.outer matmul = np.matmul vstack = np.vstack arange = np.arange @@ -20,6 +19,5 @@ shape = np.shape where = np.where - if __name__ == '__main__': bk_numba_overload diff --git a/brainpy/backend/operators/standard.py b/brainpy/backend/operators/standard.py index db364338..916a57fa 100644 --- a/brainpy/backend/operators/standard.py +++ b/brainpy/backend/operators/standard.py @@ -7,6 +7,56 @@ functions for computation backends. import numpy as np +__all__ = [ + # random function + 'normal', + + # arithmetic operation + 'sum', + 'exp', + 'matmul', + + # tensor creation + 'eye', + 'zeros', + 'ones', + 'arange', + 'as_tensor', + + # tensor manipulation + 'vstack', + + # others + 'shape', + 'reshape', +] + + +def normal(loc=0.0, scale=1.0, size=None): + """The normal operation. We expect "normal" function will behave like "numpy.random.normal" + + Draw random samples from a normal (Gaussian) distribution. + + Parameters + ---------- + loc : float or array_like of floats + Mean ("centre") of the distribution. + scale : float or array_like of floats + Standard deviation (spread or "width") of the distribution. Must be + non-negative. + size : int or tuple of ints, optional + Output shape. If the given shape is, e.g., ``(m, n, k)``, then + ``m * n * k`` samples are drawn. If size is ``None`` (default), + a single value is returned if ``loc`` and ``scale`` are both scalars. + Otherwise, ``np.broadcast(loc, scale).size`` samples are drawn. + + Returns + ------- + out : ndarray or scalar + Drawn samples from the parameterized normal distribution. + """ + pass + def sum(tensor, axis=None): """The sum operation. We expect "sum" function will behave like "numpy.sum" @@ -57,3 +107,233 @@ def sum(tensor, axis=None): pass +def exp(x): + """The exp operation. We expect "exp" function will behave like "numpy.exp" + + Parameters + ---------- + x : array_like + Input values. + + Returns + ------- + out : ndarray or scalar + Output array, element-wise exponential of `x`. + This is a scalar if `x` is a scalar. + """ + pass + + +def eye(N, *args, **kwargs): + """The eye operation. We expect "eye" function will behave like "numpy.eye". + + Return a 2-D array with ones on the diagonal and zeros elsewhere. + + Parameters + ---------- + N : int + Number of rows in the output. + + Returns + ------- + I : tensor of shape (N,N) + A tensor where all elements are equal to zero, except for the `k`-th + diagonal, whose values are equal to one. + """ + pass + + +def matmul(x1, x2, *args, **kwargs): + """The matmul operation. We expect "matmul" function will behave like "numpy.matmul". + + Parameters + ---------- + x1, x2 : array_like + Input arrays, scalars not allowed. + + Returns + ------- + y : tensor + The matrix product of the inputs. + This is a scalar only when both x1, x2 are 1-d vectors. + """ + pass + + +def vstack(tup): + """The vstack operation. We expect "vstack" function will behave like "numpy.vstack". + + Stack arrays in sequence vertically (row wise). + + Parameters + ---------- + tup : sequence of tensors + The arrays must have the same shape along all but the first axis. + 1-D arrays must have the same length. + + Returns + ------- + stacked : tensor + The tensor formed by stacking the given tensors, will be at least 2-D. + + Examples + -------- + >>> a = np.array([1, 2, 3]) + >>> b = np.array([2, 3, 4]) + >>> np.vstack((a,b)) + array([[1, 2, 3], + [2, 3, 4]]) + + >>> a = np.array([[1], [2], [3]]) + >>> b = np.array([[2], [3], [4]]) + >>> np.vstack((a,b)) + array([[1], + [2], + [3], + [2], + [3], + [4]]) + """ + pass + + +def zeros(shape, dtype=None): + """The zeros operation. We expect "zeros" function will behave like "numpy.zeros". + + Return a new array of given shape and type, filled with zeros. + + Parameters + ---------- + shape : shape : int or tuple of ints + Shape of the new array, e.g., ``(2, 3)`` or ``2``. + dtype : data-type, optional + The desired data-type for the array, e.g., `int`. Default is + `float64`. + + Returns + ------- + out : tensors + Array of zeros with the given shape and dtype. + """ + pass + + +def ones(shape, dtype=None): + """The ones operation. We expect "ones" function will behave like "numpy.ones". + + Return a new array of given shape and type, filled with ones. + + Parameters + ---------- + shape : shape : int or tuple of ints + Shape of the new array, e.g., ``(2, 3)`` or ``2``. + dtype : data-type, optional + The desired data-type for the array, e.g., `int`. Default is + `float64`. + + Returns + ------- + out : tensors + Array of ones with the given shape and dtype. + """ + pass + + +def arange(start=None, *args, **kwargs): + """The arange operation. We expect "arange" function will behave like "numpy.arange". + + Return evenly spaced values within a given interval. + + Parameters + ---------- + start : number, optional + Start of interval. The interval includes this value. The default + start value is 0. + stop : number + End of interval. The interval does not include this value, except + in some cases where `step` is not an integer and floating point + round-off affects the length of `out`. + step : number, optional + Spacing between values. For any output `out`, this is the distance + between two adjacent values, ``out[i+1] - out[i]``. The default + step size is 1. If `step` is specified as a position argument, + `start` must also be given. + dtype : dtype + The type of the output array. If `dtype` is not given, infer the data + type from the other input arguments. + + Returns + ------- + arange : ndarray + Array of evenly spaced values. + + For floating point arguments, the length of the result is + ``ceil((stop - start)/step)``. Because of floating point overflow, + this rule may result in the last element of `out` being greater + than `stop`. + """ + pass + + +def reshape(a, newshape): + """The reshape operation. We expect "reshape" function will behave like "numpy.reshape". + + Gives a new shape to an array without changing its data. + + Parameters + ---------- + a : array_like + Array to be reshaped. + newshape : int or tuple of ints + The new shape should be compatible with the original shape. If + an integer, then the result will be a 1-D array of that length. + One shape dimension can be -1. In this case, the value is + inferred from the length of the array and remaining dimensions. + + Returns + ------- + reshaped_array : ndarray + This will be a new view object if possible; otherwise, it will + be a copy. Note there is no guarantee of the *memory layout* (C- or + Fortran- contiguous) of the returned array. + """ + pass + + +def shape(a): + """The shape operation. We expect "shape" function will behave like "numpy.shape". + + Parameters + ---------- + a : array_like + Input array. + + Returns + ------- + shape : tuple of ints + The elements of the shape tuple give the lengths of the + corresponding array dimensions. + """ + pass + + +def as_tensor(a, dtype=None): + """The as_tensor operation. We expect "as_tensor" function will behave like "numpy.asarray". + + Parameters + ---------- + a : array_like + Input data, in any form that can be converted to an array. This + includes lists, lists of tuples, tuples, tuples of tuples, tuples + of lists and ndarrays. + dtype : data-type, optional + By default, the data-type is inferred from the input data. + + Returns + ------- + out : ndarray + Array interpretation of `a`. No copy is performed if the input + is already an ndarray with matching dtype and order. If `a` is a + subclass of ndarray, a base class ndarray is returned. + """ + pass diff --git a/brainpy/backend/runners/numba_cuda_runner.py b/brainpy/backend/runners/numba_cuda_runner.py index add5f9c4..0b94e4ce 100644 --- a/brainpy/backend/runners/numba_cuda_runner.py +++ b/brainpy/backend/runners/numba_cuda_runner.py @@ -4,7 +4,7 @@ import ast from brainpy import backend from brainpy import tools -from brainpy.simulation.population import SynConn, NeuGroup +from brainpy.simulation.brain_objects import SynConn, NeuGroup from .numba_cpu_runner import NumbaCPUNodeRunner from .numba_cpu_runner import StepFuncReader diff --git a/brainpy/integrators/ode/rk_adaptive_methods.py b/brainpy/integrators/ode/rk_adaptive_methods.py index 8ef27cbc..45ff5c42 100644 --- a/brainpy/integrators/ode/rk_adaptive_methods.py +++ b/brainpy/integrators/ode/rk_adaptive_methods.py @@ -1,10 +1,5 @@ # -*- coding: utf-8 -*- -""" -https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods -https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods -""" - from brainpy import backend from brainpy.integrators import constants from .wrapper import adaptive_rk_wrapper @@ -108,7 +103,6 @@ def rkf45(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=Non adaptive=adaptive, show_code=show_code, var_type=var_type) - def rkf12(f=None, tol=None, adaptive=None, dt=None, show_code=None, var_type=None): """The Fehlberg RK1(2) method for ordinary differential equations. diff --git a/brainpy/integrators/ode/rk_methods.py b/brainpy/integrators/ode/rk_methods.py index 8f2b6116..992fc01e 100644 --- a/brainpy/integrators/ode/rk_methods.py +++ b/brainpy/integrators/ode/rk_methods.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -""" -https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods#Kutta's_third-order_method -""" from brainpy import backend from .wrapper import rk_wrapper @@ -126,8 +123,6 @@ def ralston2(f=None, show_code=None, dt=None): return _base(A=A, B=B, C=C, f=f, show_code=show_code, dt=dt) - - def rk2(f=None, show_code=None, dt=None, beta=None): """Runge–Kutta methods for ordinary differential equations. diff --git a/brainpy/integrators/ode/wrapper.py b/brainpy/integrators/ode/wrapper.py index fe9d9e49..27fafa1a 100644 --- a/brainpy/integrators/ode/wrapper.py +++ b/brainpy/integrators/ode/wrapper.py @@ -320,4 +320,3 @@ def wrapper_of_rk2(f, show_code, dt, beta): return _compile_and_assign_attrs( code_lines=code_lines, code_scope=code_scope, show_code=show_code, func_name=func_name, variables=variables, parameters=parameters, dt=dt) - diff --git a/brainpy/integrators/sde/__init__.py b/brainpy/integrators/sde/__init__.py index eb0dbc9f..9632f714 100644 --- a/brainpy/integrators/sde/__init__.py +++ b/brainpy/integrators/sde/__init__.py @@ -6,6 +6,6 @@ Numerical methods for stochastic differential equations. from .euler_and_milstein import * from .srk_scalar import * -from .srk_strong import * +# from .srk_strong import * # from .srk_weak import * diff --git a/brainpy/simulation/__init__.py b/brainpy/simulation/__init__.py index 20fa232a..c7722cd2 100644 --- a/brainpy/simulation/__init__.py +++ b/brainpy/simulation/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -from .network import * -from .population import * +from .brain_objects import * + diff --git a/brainpy/simulation/brain_objects.py b/brainpy/simulation/brain_objects.py new file mode 100644 index 00000000..35bf5537 --- /dev/null +++ b/brainpy/simulation/brain_objects.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- + +from collections import OrderedDict + +from brainpy import backend +from brainpy import errors +from brainpy.simulation import delay +from brainpy.simulation import utils +from brainpy.simulation.dynamic_system import DynamicSystem + +__all__ = [ + 'NeuGroup', + 'SynConn', + 'TwoEndConn', + 'Network', +] + +_NeuGroup_NO = 0 +_TwoEndSyn_NO = 0 + + +class NeuGroup(DynamicSystem): + """Neuron Group. + + Parameters + ---------- + steps : NeuType + The instantiated neuron type model. + size : int, tuple + The neuron group geometry. + monitors : list, tuple + Variables to monitor. + name : str + The name of the neuron group. + """ + + def __init__(self, size, monitors=None, name=None, show_code=False): + # name + # ----- + if name is None: + name = '' + else: + name = '_' + name + global _NeuGroup_NO + _NeuGroup_NO += 1 + name = f'NG{_NeuGroup_NO}{name}' + + # size + # ---- + if isinstance(size, (list, tuple)): + if len(size) <= 0: + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + if not isinstance(size[0], int): + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + size = tuple(size) + elif isinstance(size, int): + size = (size,) + else: + raise errors.ModelDefError('size must be int, or a tuple/list of int.') + self.size = size + + # initialize + # ---------- + super(NeuGroup, self).__init__(steps={'update': self.update}, + monitors=monitors, + name=name, + show_code=show_code) + + def update(self, *args): + raise NotImplementedError + + +class SynConn(DynamicSystem): + """Synaptic Connections. + """ + + def __init__(self, steps, monitors=None, name=None, show_code=False): + # check delay update + if callable(steps): + steps = OrderedDict([(steps.__name__, steps)]) + elif isinstance(steps, (tuple, list)) and callable(steps[0]): + steps = OrderedDict([(step.__name__, step) for step in steps]) + else: + assert isinstance(steps, dict) + + if hasattr(self, 'constant_delays'): + for key, delay_var in self.constant_delays.items(): + if delay_var.update not in steps: + delay_name = f'{key}_delay_update' + setattr(self, delay_name, delay_var.update) + steps[delay_name] = delay_var.update + + # initialize super class + super(SynConn, self).__init__(steps=steps, monitors=monitors, name=name, show_code=show_code) + + # delay assignment + if hasattr(self, 'constant_delays'): + for key, delay_var in self.constant_delays.items(): + delay_var.name = f'{self.name}_delay_{key}' + + def register_constant_delay(self, key, size, delay_time): + if not hasattr(self, 'constant_delays'): + self.constant_delays = {} + if key in self.constant_delays: + raise errors.ModelDefError(f'"{key}" has been registered as an constant delay.') + self.constant_delays[key] = delay.ConstantDelay(size, delay_time) + return self.constant_delays[key] + + def update(self, *args): + raise NotImplementedError + + +class TwoEndConn(SynConn): + """Two End Synaptic Connections. + + Parameters + ---------- + steps : SynType + The instantiated neuron type model. + pre : neurons.NeuGroup, neurons.NeuSubGroup + Pre-synaptic neuron group. + post : neurons.NeuGroup, neurons.NeuSubGroup + Post-synaptic neuron group. + monitors : list, tuple + Variables to monitor. + name : str + The name of the neuron group. + """ + + def __init__(self, pre, post, monitors=None, name=None, show_code=False): + # name + # ---- + if name is None: + name = '' + else: + name = '_' + name + global _TwoEndSyn_NO + _TwoEndSyn_NO += 1 + name = f'TEC{_TwoEndSyn_NO}{name}' + + # pre or post neuron group + # ------------------------ + if not isinstance(pre, NeuGroup): + raise errors.ModelUseError('"pre" must be an instance of NeuGroup.') + self.pre = pre + if not isinstance(post, NeuGroup): + raise errors.ModelUseError('"post" must be an instance of NeuGroup.') + self.post = post + + # initialize + # ---------- + super(TwoEndConn, self).__init__(steps={'update': self.update}, + name=name, + monitors=monitors, + show_code=show_code) + + +class Network(object): + """The main simulation controller in ``BrainPy``. + + ``Network`` handles the running of a simulation. It contains a set + of objects that are added with `add()`. The `run()` method actually + runs the simulation. The main loop runs according to user add orders. + The objects in the `Network` are accessible via their names, e.g. + `net.name` would return the `object`. + """ + + def __init__(self, *args, show_code=False, **kwargs): + # record the current step + self.t_start = 0. + self.t_end = 0. + + # store all nodes + self.all_nodes = OrderedDict() + + # store the step function + self.run_func = None + self.show_code = show_code + + # add nodes + self.add(*args, **kwargs) + + def __getattr__(self, item): + if item in self.all_nodes: + return self.all_nodes[item] + else: + return super(Network, self).__getattribute__(item) + + def _add_obj(self, obj, name=None): + # 1. check object type + if not isinstance(obj, DynamicSystem): + raise ValueError(f'Unknown object type "{type(obj)}". ' + f'Currently, Network only supports ' + f'{NeuGroup.__name__} and ' + f'{TwoEndConn.__name__}.') + # 2. check object name + name = obj.name if name is None else name + if name in self.all_nodes: + raise KeyError(f'Name "{name}" has been used in the network, ' + f'please change another name.') + # 3. add object to the network + self.all_nodes[name] = obj + if obj.name != name: + self.all_nodes[obj.name] = obj + + def add(self, *args, **kwargs): + """Add object (neurons or synapses) to the network. + + Parameters + ---------- + args + The nameless objects. + kwargs + The named objects, which can be accessed by `net.xxx` + (xxx is the name of the object). + """ + for obj in args: + self._add_obj(obj) + for name, obj in kwargs.items(): + self._add_obj(obj, name) + + def run(self, duration, inputs=(), report=False, report_percent=0.1): + """Run the simulation for the given duration. + + This function provides the most convenient way to run the network. + For example: + + Parameters + ---------- + duration : int, float, tuple, list + The amount of simulation time to run for. + inputs : list, tuple + The receivers, external inputs and durations. + report : bool + Report the progress of the simulation. + report_percent : float + The speed to report simulation progress. + """ + # preparation + start, end = utils.check_duration(duration) + dt = backend.get_dt() + ts = backend.arange(start, end, dt) + + # build the network + run_length = ts.shape[0] + format_inputs = utils.format_net_level_inputs(inputs, run_length) + net_runner = backend.get_net_runner()(all_nodes=self.all_nodes) + self.run_func = net_runner.build(run_length=run_length, + formatted_inputs=format_inputs, + return_code=False, + show_code=self.show_code) + + # run the network + utils.run_model(self.run_func, times=ts, report=report, report_percent=report_percent) + + # end + self.t_start, self.t_end = start, end + for obj in self.all_nodes.values(): + if len(obj.mon['vars']) > 0: + obj.mon['ts'] = ts + + @property + def ts(self): + """Get the time points of the network. + """ + return backend.arange(self.t_start, self.t_end, backend.get_dt()) diff --git a/brainpy/simulation/dynamic_system.py b/brainpy/simulation/dynamic_system.py new file mode 100644 index 00000000..a7099047 --- /dev/null +++ b/brainpy/simulation/dynamic_system.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +from collections import OrderedDict + +from brainpy import backend +from brainpy import errors +from brainpy.simulation import utils +from brainpy.simulation.monitors import Monitor + +__all__ = [ + 'DynamicSystem', +] + +_DynamicModel_NO = 0 + + +class DynamicSystem(object): + """Base Dynamic System Class. + + Parameters + ---------- + name : str + The name of the dynamic system. + steps : callable, list of callable, dict + The callable function, or a list of callable functions. + monitors : list, tuple, None + Variables to monitor. + """ + + target_backend = None + + def __init__(self, steps, monitors=None, name=None, host=None, show_code=False): + # host of the data + # ---------------- + if host is None: + host = self + self.host = host + + # model + # ----- + if callable(steps): + self.steps = OrderedDict([(steps.__name__, steps)]) + elif isinstance(steps, (list, tuple)) and callable(steps[0]): + self.steps = OrderedDict([(step.__name__, step) for step in steps]) + elif isinstance(steps, dict): + self.steps = steps + else: + raise errors.ModelDefError(f'Unknown model type: {type(steps)}. Currently, BrainPy ' + f'only supports: function, list/tuple/dict of functions.') + + # name + # ---- + if name is None: + global _DynamicModel_NO + name = f'DM{_DynamicModel_NO}' + _DynamicModel_NO += 1 + if not name.isidentifier(): + raise errors.ModelUseError(f'"{name}" isn\'t a valid identifier according to Python ' + f'language definition. Please choose another name.') + self.name = name + + # monitors + # --------- + if monitors is None: + monitors = [] + self.mon = Monitor(monitors) + for var in self.mon['vars']: + if not hasattr(self, var): + raise errors.ModelDefError(f"Item {var} isn't defined in model {self}, " + f"so it can not be monitored.") + + # runner + # ------- + self.runner = backend.get_node_runner()(pop=self) + + # run function + # ------------ + self.run_func = None + + # others + # --- + self.show_code = show_code + if self.target_backend is None: + raise errors.ModelDefError('Must define "target_backend".') + if isinstance(self.target_backend, str): + self._target_backend = (self.target_backend,) + elif isinstance(self.target_backend, (tuple, list)): + if not isinstance(self.target_backend[0], str): + raise errors.ModelDefError('"target_backend" must be a list/tuple of string.') + self._target_backend = tuple(self.target_backend) + else: + raise errors.ModelDefError(f'Unknown setting of "target_backend": {self.target_backend}') + + def build(self, inputs, input_is_formatted=False, return_code=True, mon_length=0, show_code=False): + """Build the object for running. + + Parameters + ---------- + inputs : list, tuple, optional + The object inputs. + return_code : bool + Whether return the formatted codes. + mon_length : int + The monitor length. + + Returns + ------- + calls : list, tuple + The code lines to call step functions. + """ + if (self._target_backend[0] != 'general') and \ + (backend.get_backend() not in self._target_backend): + raise errors.ModelDefError(f'The model {self.name} is target to run on {self._target_backend},' + f'but currently the default backend of BrainPy is ' + f'{backend.get_backend()}') + if not input_is_formatted: + inputs = utils.format_pop_level_inputs(inputs, self, mon_length) + return self.runner.build(formatted_inputs=inputs, + mon_length=mon_length, + return_code=return_code, + show_code=(self.show_code or show_code)) + + def run(self, duration, inputs=(), report=False, report_percent=0.1): + """The running function. + + Parameters + ---------- + duration : float, int, tuple, list + The running duration. + inputs : list, tuple + The model inputs with the format of ``[(key, value [operation])]``. + report : bool + Whether report the running progress. + report_percent : float + The percent of progress to report. + """ + + # times + # ------ + start, end = utils.check_duration(duration) + times = backend.arange(start, end, backend.get_dt()) + run_length = backend.shape(times)[0] + + # build run function + # ------------------ + self.run_func = self.build(inputs, input_is_formatted=False, mon_length=run_length, return_code=False) + + # run the model + # ------------- + utils.run_model(self.run_func, times, report, report_percent) + self.mon['ts'] = times + + def get_schedule(self): + """Get the schedule (running order) of the update functions. + + Returns + ------- + schedule : list, tuple + The running order of update functions. + """ + return self.runner.get_schedule() + + def set_schedule(self, schedule): + """Set the schedule (running order) of the update functions. + + For example, if the ``self.model`` has two step functions: `step1`, `step2`. + Then, you can set the shedule by using: + + >>> pop = DynamicSystem(...) + >>> pop.set_schedule(['input', 'step1', 'step2', 'monitor']) + """ + self.runner.set_schedule(schedule) + + def __str__(self): + return self.name diff --git a/brainpy/simulation/network.py b/brainpy/simulation/network.py deleted file mode 100644 index c021197a..00000000 --- a/brainpy/simulation/network.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- - -from collections import OrderedDict - -from brainpy import backend -from brainpy.simulation import population -from brainpy.simulation import utils - -__all__ = [ - 'Network', -] - - -class Network(object): - """The main simulation controller in ``BrainPy``. - - ``Network`` handles the running of a simulation. It contains a set - of objects that are added with `add()`. The `run()` method actually - runs the simulation. The main loop runs according to user add orders. - The objects in the `Network` are accessible via their names, e.g. - `net.name` would return the `object`. - """ - - def __init__(self, *args, show_code=False, **kwargs): - # record the current step - self.t_start = 0. - self.t_end = 0. - - # store all nodes - self.all_nodes = OrderedDict() - - # store the step function - self.run_func = None - self.show_code = show_code - - # add nodes - self.add(*args, **kwargs) - - def __getattr__(self, item): - if item in self.all_nodes: - return self.all_nodes[item] - else: - return super(Network, self).__getattribute__(item) - - def _add_obj(self, obj, name=None): - # 1. check object type - if not isinstance(obj, population.Population): - raise ValueError(f'Unknown object type "{type(obj)}". ' - f'Currently, Network only supports ' - f'{population.NeuGroup.__name__} and ' - f'{population.TwoEndConn.__name__}.') - # 2. check object name - name = obj.name if name is None else name - if name in self.all_nodes: - raise KeyError(f'Name "{name}" has been used in the network, ' - f'please change another name.') - # 3. add object to the network - self.all_nodes[name] = obj - if obj.name != name: - self.all_nodes[obj.name] = obj - - def add(self, *args, **kwargs): - """Add object (neurons or synapses) to the network. - - Parameters - ---------- - args - The nameless objects. - kwargs - The named objects, which can be accessed by `net.xxx` - (xxx is the name of the object). - """ - for obj in args: - self._add_obj(obj) - for name, obj in kwargs.items(): - self._add_obj(obj, name) - - def run(self, duration, inputs=(), report=False, report_percent=0.1): - """Run the simulation for the given duration. - - This function provides the most convenient way to run the network. - For example: - - Parameters - ---------- - duration : int, float, tuple, list - The amount of simulation time to run for. - inputs : list, tuple - The receivers, external inputs and durations. - report : bool - Report the progress of the simulation. - report_percent : float - The speed to report simulation progress. - """ - # preparation - start, end = utils.check_duration(duration) - dt = backend.get_dt() - ts = backend.arange(start, end, dt) - - # build the network - run_length = ts.shape[0] - format_inputs = utils.format_net_level_inputs(inputs, run_length) - net_runner = backend.get_net_runner()(all_nodes=self.all_nodes) - self.run_func = net_runner.build(run_length=run_length, - formatted_inputs=format_inputs, - return_code=False, - show_code=self.show_code) - - # run the network - utils.run_model(self.run_func, times=ts, report=report, report_percent=report_percent) - - # end - self.t_start, self.t_end = start, end - for obj in self.all_nodes.values(): - if len(obj.mon['vars']) > 0: - obj.mon['ts'] = ts - - @property - def ts(self): - """Get the time points of the network. - """ - return backend.arange(self.t_start, self.t_end, backend.get_dt()) diff --git a/brainpy/simulation/population.py b/brainpy/simulation/population.py deleted file mode 100644 index 873dad4a..00000000 --- a/brainpy/simulation/population.py +++ /dev/null @@ -1,317 +0,0 @@ -# -*- coding: utf-8 -*- - -from collections import OrderedDict - -from brainpy import backend -from brainpy import errors -from brainpy.simulation import constants -from brainpy.simulation import delay -from brainpy.simulation import utils -from brainpy.simulation.monitors import Monitor - -__all__ = [ - 'Population', - 'NeuGroup', - 'SynConn', - 'TwoEndConn', -] - -_POPULATION_NO = 0 -_NeuGroup_NO = 0 -_TwoEndSyn_NO = 0 - - -class Population(object): - """Base Population Class. - - Parameters - ---------- - name : str - The name of the (neurons/synapses) ensemble. - steps : callable, list of callable - The callable function, or a list of callable functions. - monitors : list, tuple, None - Variables to monitor. - pop_type : str - Class type. - """ - - target_backend = None - - def __init__(self, steps, monitors=None, pop_type=None, name=None, host=None, show_code=False): - # host of the data - # ---------------- - if host is None: - host = self - self.host = host - - # ensemble type - # ------------- - if pop_type is None: - pop_type = constants.UNKNOWN_TYPE - if pop_type not in constants.SUPPORTED_TYPES: - print(f'Ensemble type {pop_type} is not registered in BrainPy. Currently, ' - f'BrainPy has recognized "{constants.SUPPORTED_TYPES}".') - self.ensemble_type = pop_type - - # model - # ----- - if callable(steps): - self.steps = OrderedDict([(steps.__name__, steps)]) - elif isinstance(steps, (list, tuple)) and callable(steps[0]): - self.steps = OrderedDict([(step.__name__, step) for step in steps]) - elif isinstance(steps, dict): - self.steps = steps - else: - raise errors.ModelDefError(f'Unknown model type: {type(steps)}. Currently, BrainPy ' - f'only supports: function, list/tuple/dict of functions.') - - # name - # ---- - if name is None: - global _POPULATION_NO - name = f'POP{_POPULATION_NO}' - _POPULATION_NO += 1 - if not name.isidentifier(): - raise errors.ModelUseError( - f'"{name}" isn\'t a valid identifier according to Python ' - f'language definition. Please choose another name.') - self.name = name - - # monitors - # --------- - if monitors is None: - monitors = [] - self.mon = Monitor(monitors) - for var in self.mon['vars']: - if not hasattr(self, var): - raise errors.ModelDefError(f"Item {var} isn't defined in model {self}, " - f"so it can not be monitored.") - - # runner - # ------- - self.runner = backend.get_node_runner()(pop=self) - - # run function - # ------------ - self.run_func = None - - # others - # --- - self.show_code = show_code - if self.target_backend is None: - raise errors.ModelDefError('Must define "target_backend".') - if isinstance(self.target_backend, str): - self.target_backend = [self.target_backend] - assert isinstance(self.target_backend, (tuple, list)), 'target_backend must be a list/tuple.' - - def build(self, inputs, input_is_formatted=False, return_code=True, mon_length=0, show_code=False): - """Build the object for running. - - Parameters - ---------- - inputs : list, tuple, optional - The object inputs. - return_code : bool - Whether return the formatted codes. - mon_length : int - The monitor length. - - Returns - ------- - calls : list, tuple - The code lines to call step functions. - """ - if (self.target_backend[0] != 'general') and (backend.get_backend() not in self.target_backend): - raise errors.ModelDefError(f'The model {self.name} is target to run on {self.target_backend},' - f'but currently the default backend of BrainPy is ' - f'{backend.get_backend()}') - if not input_is_formatted: - inputs = utils.format_pop_level_inputs(inputs, self, mon_length) - return self.runner.build(formatted_inputs=inputs, - mon_length=mon_length, - return_code=return_code, - show_code=(self.show_code or show_code)) - - def run(self, duration, inputs=(), report=False, report_percent=0.1): - """The running function. - - Parameters - ---------- - duration : float, int, tuple, list - The running duration. - inputs : list, tuple - The model inputs with the format of ``[(key, value [operation])]``. - report : bool - Whether report the running progress. - report_percent : float - The percent of progress to report. - """ - - # times - # ------ - start, end = utils.check_duration(duration) - times = backend.arange(start, end, backend.get_dt()) - run_length = backend.shape(times)[0] - - # build run function - # ------------------ - self.run_func = self.build(inputs, input_is_formatted=False, mon_length=run_length, return_code=False) - - # run the model - # ------------- - utils.run_model(self.run_func, times, report, report_percent) - self.mon['ts'] = times - - def get_schedule(self): - """Get the schedule (running order) of the update functions. - - Returns - ------- - schedule : list, tuple - The running order of update functions. - """ - return self.runner.get_schedule() - - def set_schedule(self, schedule): - """Set the schedule (running order) of the update functions. - - For example, if the ``self.model`` has two step functions: `step1`, `step2`. - Then, you can set the shedule by using: - - >>> pop = Population(...) - >>> pop.set_schedule(['input', 'step1', 'step2', 'monitor']) - """ - self.runner.set_schedule(schedule) - - def __str__(self): - return self.name - - -class NeuGroup(Population): - """Neuron Group. - - Parameters - ---------- - steps : NeuType - The instantiated neuron type model. - size : int, tuple - The neuron group geometry. - monitors : list, tuple - Variables to monitor. - name : str - The name of the neuron group. - """ - - def __init__(self, steps, size, monitors=None, name=None, host=None, show_code=False): - # name - # ----- - if name is None: - name = '' - else: - name = '_' + name - global _NeuGroup_NO - _NeuGroup_NO += 1 - name = f'NG{_NeuGroup_NO}{name}' - - # size - # ---- - if isinstance(size, (list, tuple)): - if len(size) <= 0: - raise errors.ModelDefError('size must be int, or a tuple/list of int.') - if not isinstance(size[0], int): - raise errors.ModelDefError('size must be int, or a tuple/list of int.') - size = tuple(size) - elif isinstance(size, int): - size = (size,) - else: - raise errors.ModelDefError('size must be int, or a tuple/list of int.') - self.size = size - - # initialize - # ---------- - super(NeuGroup, self).__init__(steps=steps, - monitors=monitors, - name=name, - host=host, - pop_type=constants.NEU_GROUP_TYPE, - show_code=show_code) - - -class SynConn(Population): - """Synaptic Connections. - """ - - def __init__(self, steps, **kwargs): - # check delay update - if callable(steps): - steps = OrderedDict([(steps.__name__, steps)]) - elif isinstance(steps, (tuple, list)) and callable(steps[0]): - steps = OrderedDict([(step.__name__, step) for step in steps]) - else: - assert isinstance(steps, dict) - if hasattr(self, 'constant_delays'): - for key, delay_var in self.constant_delays.items(): - if delay_var.update not in steps: - delay_name = f'{key}_delay_update' - setattr(self, delay_name, delay_var.update) - steps[delay_name] = delay_var.update - super(SynConn, self).__init__(steps=steps, **kwargs) - - for key, delay_var in self.constant_delays.items(): - delay_var.name = f'{self.name}_delay_{key}' - - def register_constant_delay(self, key, size, delay_time): - if not hasattr(self, 'constant_delays'): - self.constant_delays = {} - if key in self.constant_delays: - raise errors.ModelDefError(f'"{key}" has been registered as an constant delay.') - self.constant_delays[key] = delay.ConstantDelay(size, delay_time) - return self.constant_delays[key] - - -class TwoEndConn(SynConn): - """Two End Synaptic Connections. - - Parameters - ---------- - steps : SynType - The instantiated neuron type model. - pre : neurons.NeuGroup, neurons.NeuSubGroup - Pre-synaptic neuron group. - post : neurons.NeuGroup, neurons.NeuSubGroup - Post-synaptic neuron group. - monitors : list, tuple - Variables to monitor. - name : str - The name of the neuron group. - """ - - def __init__(self, steps, pre, post, monitors=None, name=None, host=None, show_code=False): - # name - # ---- - if name is None: - name = '' - else: - name = '_' + name - global _TwoEndSyn_NO - _TwoEndSyn_NO += 1 - name = f'TEC{_TwoEndSyn_NO}{name}' - - # pre or post neuron group - # ------------------------ - if not isinstance(pre, NeuGroup): - raise errors.ModelUseError('"pre" must be an instance of NeuGroup.') - self.pre = pre - if not isinstance(post, NeuGroup): - raise errors.ModelUseError('"post" must be an instance of NeuGroup.') - self.post = post - - # initialize - # ---------- - super(TwoEndConn, self).__init__(steps=steps, - name=name, - monitors=monitors, - pop_type=constants.TWO_END_TYPE, - host=host, - show_code=show_code) diff --git a/brainpy/simulation/utils.py b/brainpy/simulation/utils.py index e96d8881..d70106e7 100644 --- a/brainpy/simulation/utils.py +++ b/brainpy/simulation/utils.py @@ -173,14 +173,14 @@ def format_net_level_inputs(inputs, run_length): formatted_input : dict The formatted input. """ - from brainpy.simulation import population + from brainpy.simulation import brain_objects # 1. format the inputs to standard # formats and check the inputs if not isinstance(inputs, (tuple, list)): raise errors.ModelUseError('"inputs" must be a tuple/list.') if len(inputs) > 0 and not isinstance(inputs[0], (list, tuple)): - if isinstance(inputs[0], population.Population): + if isinstance(inputs[0], brain_objects.DynamicSystem): inputs = [inputs] else: raise errors.ModelUseError('Unknown input structure. Only supports ' @@ -199,7 +199,7 @@ def format_net_level_inputs(inputs, run_length): formatted_inputs = {} for input in inputs: # target - if isinstance(input[0], population.Population): + if isinstance(input[0], brain_objects.DynamicSystem): target = input[0] target_name = input[0].name else: diff --git a/docs/Makefile b/docs/Makefile index 6a354848..2de4207a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,7 +5,7 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build -SPHINXPROJ = npbrain +SPHINXPROJ = brainpy SOURCEDIR = . BUILDDIR = _build diff --git a/examples/neurons/FitzHugh_Nagumo.py b/examples/neurons/FitzHugh_Nagumo.py index 2c3bac16..b48413b0 100644 --- a/examples/neurons/FitzHugh_Nagumo.py +++ b/examples/neurons/FitzHugh_Nagumo.py @@ -19,8 +19,7 @@ class FitzHughNagumo(bp.NeuGroup): self.spike = bp.backend.zeros(size) self.input = bp.backend.zeros(size) - super(FitzHughNagumo, self).__init__( - size=size, steps=[self.update], **kwargs) + super(FitzHughNagumo, self).__init__(size=size, **kwargs) @staticmethod @bp.odeint(method='rk4') diff --git a/examples/neurons/HH_model.py b/examples/neurons/HH_model.py index fe1ca116..b328b5e0 100644 --- a/examples/neurons/HH_model.py +++ b/examples/neurons/HH_model.py @@ -30,7 +30,7 @@ class HH(bp.NeuGroup): self.spike = bp.backend.zeros(size) self.input = bp.backend.zeros(size) - super(HH, self).__init__(size=size, steps=[self.update], **kwargs) + super(HH, self).__init__(size=size, **kwargs) @staticmethod @bp.odeint(method='rk4', show_code=True) diff --git a/examples/others/lorenz_system.py b/examples/others/lorenz_system.py index 98bdaf32..bf04957d 100644 --- a/examples/others/lorenz_system.py +++ b/examples/others/lorenz_system.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D @@ -7,7 +8,7 @@ import brainpy as bp bp.backend.set('numpy', dt=0.005) -class LorenzSystem(bp.Population): +class LorenzSystem(bp.DynamicSystem): target_backend = 'general' def __init__(self, size=0, sigma=10, beta=8 / 3, rho=28, p=0.1, **kwargs): @@ -23,7 +24,8 @@ class LorenzSystem(bp.Population): def lorenz_g(x, y, z, t, sigma, rho, beta, p): return p * x, p * y, p * z - @bp.sdeint(g=lorenz_g, sde_type=bp.ITO_SDE, wiener_type=bp.SCALAR_WIENER) + @bp.sdeint(g=lorenz_g, sde_type=bp.ITO_SDE, + wiener_type=bp.SCALAR_WIENER) def lorenz_f(x, y, z, t, sigma, rho, beta, p): dx = sigma * (y - x) dy = x * (rho - z) - y @@ -50,6 +52,5 @@ ax.set_xlabel('y') ax.set_xlabel('z') plt.show() - if __name__ == '__main__': Axes3D diff --git a/examples/synapses/AMPA_synapse.py b/examples/synapses/AMPA_synapse.py index 20e98bfc..b78d4911 100644 --- a/examples/synapses/AMPA_synapse.py +++ b/examples/synapses/AMPA_synapse.py @@ -33,7 +33,7 @@ class HH(bp.NeuGroup): self.spike = np.zeros(size) self.input = np.zeros(size) - super(HH, self).__init__(size=size, steps=[self.update], **kwargs) + super(HH, self).__init__(size=size, **kwargs) @staticmethod @bp.odeint @@ -88,8 +88,7 @@ class AMPA1_vec(bp.TwoEndConn): self.s = bp.backend.zeros(self.size) self.g = self.register_constant_delay('g', size=self.size, delay_time=delay) - super(AMPA1_vec, self).__init__(steps=[self.update, ], - pre=pre, post=post, **kwargs) + super(AMPA1_vec, self).__init__(pre=pre, post=post, **kwargs) @staticmethod @bp.odeint(method='euler') @@ -125,8 +124,7 @@ class AMPA1_mat(bp.TwoEndConn): self.s = bp.backend.zeros(self.size) self.g = self.register_constant_delay('g', size=self.size, delay_time=delay) - super(AMPA1_mat, self).__init__(steps=[self.update, ], - pre=pre, post=post, **kwargs) + super(AMPA1_mat, self).__init__(pre=pre, post=post, **kwargs) @staticmethod @bp.odeint @@ -144,7 +142,6 @@ class AMPA1_mat(bp.TwoEndConn): if __name__ == '__main__': - hh = HH(100, monitors=['V']) ampa = AMPA1_vec(pre=hh, post=hh, conn=bp.connect.All2All(), delay=10., monitors=['s']) -- 2.34.1 From 5ef091158ee378f3d70f63df81b1450655d4084a Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Mon, 22 Mar 2021 23:51:56 +0800 Subject: [PATCH 07/15] Improve NumbaCPUNodeRunner --- brainpy/analysis/base.py | 5 +- brainpy/analysis/bifurcation.py | 26 +- brainpy/analysis/dyn_model.py | 14 +- brainpy/backend/__init__.py | 31 +- brainpy/backend/operators/bk_numpy.py | 4 - brainpy/backend/runners/numba_cpu_runner.py | 178 ++++++----- brainpy/simulation/dynamic_system.py | 16 +- docs/advanced/HH_model_in_ANNarchy.ipynb | 291 ------------------ docs/advanced/gapjunction_lif_in_brian2.ipynb | 266 ---------------- examples/neurons/LIF_model.py | 48 +++ examples/synapses/AMPA_synapse.py | 4 +- tests/backend/runners/numba_cpu_runner.py | 78 ++++- 12 files changed, 281 insertions(+), 680 deletions(-) delete mode 100644 docs/advanced/HH_model_in_ANNarchy.ipynb delete mode 100644 docs/advanced/gapjunction_lif_in_brian2.ipynb create mode 100644 examples/neurons/LIF_model.py diff --git a/brainpy/analysis/base.py b/brainpy/analysis/base.py index 680b57b1..4259f73b 100644 --- a/brainpy/analysis/base.py +++ b/brainpy/analysis/base.py @@ -82,7 +82,7 @@ class BaseNeuronAnalyzer(object): # model # ----- - if isinstance(model_or_integrals, dyn_model.DynamicalModel): + if isinstance(model_or_integrals, dyn_model.DynamicModel): self.model = model_or_integrals elif (isinstance(model_or_integrals, (tuple, list)) and callable(model_or_integrals[0])) or \ callable(model_or_integrals): @@ -359,7 +359,8 @@ class Base1DNeuronAnalyzer(BaseNeuronAnalyzer): func_codes = [f'def solve_x({argument2}):'] for expr in self.x_eq_group.sub_exprs[:-1]: func_codes.append(f'{expr.var_name} = {expr.code}') - result_expr = ', '.join([sympy_analysis.sympy2str(expr) for expr in results]) + result_expr = ', '.join([sympy_analysis.sympy2str(expr) + for expr in results]) func_codes.append(f'_res_ = {result_expr}') func_codes.append(f'return np.array(_res_)') diff --git a/brainpy/analysis/bifurcation.py b/brainpy/analysis/bifurcation.py index 9e90567f..7cb8927d 100644 --- a/brainpy/analysis/bifurcation.py +++ b/brainpy/analysis/bifurcation.py @@ -542,7 +542,7 @@ class FastSlowBifurcation(object): class _FastSlowTrajectory(object): def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, **kwargs): - if isinstance(model_or_intgs, dyn_model.DynamicalModel): + if isinstance(model_or_intgs, dyn_model.DynamicModel): self.model = model_or_intgs elif (isinstance(model_or_intgs, (list, tuple)) and callable(model_or_intgs[0])) or callable(model_or_intgs): self.model = dyn_model.transform_integrals_to_analyzers(model_or_intgs) @@ -570,18 +570,18 @@ class _FastSlowTrajectory(object): self.slow_var_names = list(sorted(slow_vars.keys())) # TODO - # # cannot update dynamical parameters - # all_vars = self.fast_var_names + self.slow_var_names - # self.traj_group = simulation.NeuGroup(model_or_intgs, - # size=1, - # monitors=all_vars, - # pars_update=pars_update) - # self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, - # target_vars=all_vars, - # fixed_vars=fixed_vars) - # self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() - # if not key.startswith('_')} - # self.traj_net = simulation.Network(self.traj_group) + # cannot update dynamical parameters + all_vars = self.fast_var_names + self.slow_var_names + self.traj_group = simulation.NeuGroup(model_or_intgs, + size=1, + monitors=all_vars, + pars_update=pars_update) + self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, + target_vars=all_vars, + fixed_vars=fixed_vars) + self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() + if not key.startswith('_')} + self.traj_net = simulation.Network(self.traj_group) def plot_trajectory(self, initials, duration, plot_duration=None, inputs=(), show=False): """Plot trajectories according to the settings. diff --git a/brainpy/analysis/dyn_model.py b/brainpy/analysis/dyn_model.py index 84819bd3..d18af375 100644 --- a/brainpy/analysis/dyn_model.py +++ b/brainpy/analysis/dyn_model.py @@ -13,7 +13,7 @@ except ModuleNotFoundError: __all__ = [ 'transform_integrals_to_analyzers', - 'DynamicalModel', + 'DynamicModel', ] @@ -32,8 +32,6 @@ def transform_integrals_to_analyzers(integrals): else: integral = integral - - # original function f = integral.origin_f if Dispatcher is not None and isinstance(f, Dispatcher): @@ -70,13 +68,13 @@ def transform_integrals_to_analyzers(integrals): all_parameters.update(integral.parameters) all_scope.update(code_scope) - return DynamicalModel(analyzers=analyzers, - variables=list(all_variables), - parameters=list(all_parameters), - scopes=all_scope) + return DynamicModel(analyzers=analyzers, + variables=list(all_variables), + parameters=list(all_parameters), + scopes=all_scope) -class DynamicalModel(object): +class DynamicModel(object): def __init__(self, analyzers, variables, parameters, scopes): self.analyzers = analyzers self.variables = variables diff --git a/brainpy/backend/__init__.py b/brainpy/backend/__init__.py index 34540d59..a1e70706 100644 --- a/brainpy/backend/__init__.py +++ b/brainpy/backend/__init__.py @@ -140,19 +140,31 @@ def set_ops_from_module(module): def set_ops(**kwargs): global_vars = globals() - for key in global_vars.keys(): - if (not key.startswith('__')) and (key in kwargs): - global_vars[key] = kwargs.pop(key) - - if len(kwargs): - raise ValueError(f'Unknown operations: {list(kwargs.keys())}') + for key, value in kwargs.items(): + if key not in NEEDED_OPS: + print(f'"{key}" is not a necessary operation.') + global_vars[key] = value def get_backend(): + """Get the current backend name. + + Returns + ------- + backend : str + The name of the current backend name. + """ return _backend def get_node_runner(): + """Get the current node runner. + + Returns + ------- + node_runner + The node runner class. + """ global _node_runner if _node_runner is None: from .runners.general_runner import GeneralNodeRunner @@ -161,6 +173,13 @@ def get_node_runner(): def get_net_runner(): + """Get the current network runner. + + Returns + ------- + net_runner + The network runner. + """ global _net_runner if _net_runner is None: from .runners.general_runner import GeneralNetRunner diff --git a/brainpy/backend/operators/bk_numpy.py b/brainpy/backend/operators/bk_numpy.py index 538d1924..5457efce 100644 --- a/brainpy/backend/operators/bk_numpy.py +++ b/brainpy/backend/operators/bk_numpy.py @@ -15,8 +15,6 @@ __all__ = [ 'matmul', 'vstack', 'arange', - 'moveaxis', - 'where', ] @@ -31,8 +29,6 @@ eye = np.eye matmul = np.matmul vstack = np.vstack arange = np.arange -moveaxis = np.moveaxis -where = np.where def shape(x): diff --git a/brainpy/backend/runners/numba_cpu_runner.py b/brainpy/backend/runners/numba_cpu_runner.py index 0e1b6788..f5692a29 100644 --- a/brainpy/backend/runners/numba_cpu_runner.py +++ b/brainpy/backend/runners/numba_cpu_runner.py @@ -3,6 +3,7 @@ import ast import inspect import re +from collections import OrderedDict import numba from numba.core.dispatcher import Dispatcher @@ -71,43 +72,57 @@ class StepFuncReader(ast.NodeVisitor): self.lefts = [] self.rights = [] self.lines = [] + self.visited_nodes = set() self.host = host # get delay information self.delay_call = {} def visit_Assign(self, node, level=0): + if node not in self.visited_nodes: + prefix = ' ' * level + expr = tools.ast2code(ast.fix_missing_locations(node.value)) + targets = [] + for target in node.targets: + targets.append(tools.ast2code(ast.fix_missing_locations(target))) + _target = ' = '.join(targets) + + self.rights.append(expr) + self.lefts.append(_target) + self.lines.append(f'{prefix}{_target} = {expr}') + + self.visited_nodes.add(node) + self.generic_visit(node) - prefix = ' ' * level - expr = tools.ast2code(ast.fix_missing_locations(node.value)) - self.rights.append(expr) - targets = [] - for target in node.targets: - targets.append(tools.ast2code(ast.fix_missing_locations(target))) - _target = ' = '.join(targets) - self.lefts.append(_target) - self.lines.append(f'{prefix}{_target} = {expr}') def visit_AugAssign(self, node, level=0): + if node not in self.visited_nodes: + prefix = ' ' * level + op = tools.ast2code(ast.fix_missing_locations(node.op)) + expr = tools.ast2code(ast.fix_missing_locations(node.value)) + target = tools.ast2code(ast.fix_missing_locations(node.target)) + + self.lefts.append(target) + self.rights.append(f'{target} {op} {expr}') + self.lines.append(f"{prefix}{target} = {target} {op} {expr}") + + self.visited_nodes.add(node) + self.generic_visit(node) - prefix = ' ' * level - op = tools.ast2code(ast.fix_missing_locations(node.op)) - expr = tools.ast2code(ast.fix_missing_locations(node.value)) - target = tools.ast2code(ast.fix_missing_locations(node.target)) - self.lefts.append(target) - self.rights.append(f'{target} {op} {expr}') - self.lines.append(f"{prefix}{target} = {target} {op} {expr}") def visit_AnnAssign(self, node): raise NotImplementedError('Do not support an assignment with ' 'a type annotation in Numba backend.') def visit_node_not_assign(self, node, level=0): - prefix = ' ' * level - expr = tools.ast2code(ast.fix_missing_locations(node)) - self.lines.append(f'{prefix}{expr}') - self.lefts.append('') - self.rights.append(expr) + if node not in self.visited_nodes: + prefix = ' ' * level + expr = tools.ast2code(ast.fix_missing_locations(node)) + self.lines.append(f'{prefix}{expr}') + self.lefts.append('') + self.rights.append(expr) + self.visited_nodes.add(node) + self.generic_visit(node) def visit_Assert(self, node, level=0): @@ -197,68 +212,83 @@ class StepFuncReader(ast.NodeVisitor): self.generic_visit(node) def visit_If(self, node, level=0): - # If condition - prefix = ' ' * level - compare = tools.ast2code(ast.fix_missing_locations(node.test)) - self.rights.append(f'if {compare}:') - self.lines.append(f'{prefix}if {compare}:') - # body - for expr in node.body: - self.visit_content_in_condition_control(expr, level + 1) - - # elif - while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If): - node = node.orelse[0] + if node not in self.visited_nodes: + # If condition + prefix = ' ' * level compare = tools.ast2code(ast.fix_missing_locations(node.test)) - self.lines.append(f'{prefix}elif {compare}:') + self.rights.append(f'if {compare}:') + self.lines.append(f'{prefix}if {compare}:') + + # body for expr in node.body: self.visit_content_in_condition_control(expr, level + 1) - # else: - if len(node.orelse) > 0: - self.lines.append(f'{prefix}else:') - for expr in node.orelse: - self.visit_content_in_condition_control(expr, level + 1) + # elif + while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If): + node = node.orelse[0] + compare = tools.ast2code(ast.fix_missing_locations(node.test)) + self.lines.append(f'{prefix}elif {compare}:') + for expr in node.body: + self.visit_content_in_condition_control(expr, level + 1) + + # else: + if len(node.orelse) > 0: + self.lines.append(f'{prefix}else:') + for expr in node.orelse: + self.visit_content_in_condition_control(expr, level + 1) + + self.visited_nodes.add(node) + self.generic_visit(node) def visit_For(self, node, level=0): - prefix = ' ' * level - # target - target = tools.ast2code(ast.fix_missing_locations(node.target)) - # iter - iter = tools.ast2code(ast.fix_missing_locations(node.iter)) - self.rights.append(f'{target} in {iter}') - self.lines.append(prefix + f'for {target} in {iter}:') - # body - for expr in node.body: - self.visit_content_in_condition_control(expr, level + 1) - # else - if len(node.orelse) > 0: - self.lines.append(prefix + 'else:') - for expr in node.orelse: + if node not in self.visited_nodes: + prefix = ' ' * level + # target + target = tools.ast2code(ast.fix_missing_locations(node.target)) + # iter + iter = tools.ast2code(ast.fix_missing_locations(node.iter)) + self.rights.append(f'{target} in {iter}') + self.lines.append(prefix + f'for {target} in {iter}:') + # body + for expr in node.body: self.visit_content_in_condition_control(expr, level + 1) + # else + if len(node.orelse) > 0: + self.lines.append(prefix + 'else:') + for expr in node.orelse: + self.visit_content_in_condition_control(expr, level + 1) + + self.visited_nodes.add(node) self.generic_visit(node) def visit_While(self, node, level=0): - prefix = ' ' * level - # test - test = tools.ast2code(ast.fix_missing_locations(node.test)) - self.rights.append(test) - self.lines.append(prefix + f'while {test}:') - # body - for expr in node.body: - self.visit_content_in_condition_control(expr, level + 1) - # else - if len(node.orelse) > 0: - self.lines.append(prefix + 'else:') - for expr in node.orelse: + if node not in self.visited_nodes: + prefix = ' ' * level + # test + test = tools.ast2code(ast.fix_missing_locations(node.test)) + self.rights.append(test) + self.lines.append(prefix + f'while {test}:') + # body + for expr in node.body: self.visit_content_in_condition_control(expr, level + 1) + # else + if len(node.orelse) > 0: + self.lines.append(prefix + 'else:') + for expr in node.orelse: + self.visit_content_in_condition_control(expr, level + 1) + + self.visited_nodes.add(node) self.generic_visit(node) def visit_Raise(self, node, level=0): - prefix = ' ' * level - line = tools.ast2code(ast.fix_missing_locations(node)) - self.lines.append(prefix + line) + if node not in self.visited_nodes: + prefix = ' ' * level + line = tools.ast2code(ast.fix_missing_locations(node)) + self.lines.append(prefix + line) + + self.visited_nodes.add(node) + self.generic_visit(node) def visit_Try(self, node): raise errors.CodeError('Do not support "try" handler in Numba backend.') @@ -334,7 +364,7 @@ def analyze_step_func(host, f): analyzed_results = { 'delay_call': formatter.delay_call, - 'code_string': code_string, + 'code_string': '\n'.join(formatter.lines), 'code_scope': code_scope, 'self_data_in_right': self_data_in_right, 'self_data_without_index_in_left': self_data_without_index_in_left, @@ -443,12 +473,13 @@ def class2func(cls_func, host, func_name=None, show_code=False): # analysis analyzed_results = analyze_step_func(host=host, f=cls_func) delay_call = analyzed_results['delay_call'] - code_string = analyzed_results['code_string'] + # code_string = analyzed_results['code_string'] + main_code = analyzed_results['code_string'] code_scope = analyzed_results['code_scope'] self_data_in_right = analyzed_results['self_data_in_right'] self_data_without_index_in_left = analyzed_results['self_data_without_index_in_left'] self_data_with_index_in_left = analyzed_results['self_data_with_index_in_left'] - main_code = get_func_body_code(code_string) + # main_code = get_func_body_code(code_string) num_indent = get_num_indent(main_code) data_need_pass = sorted(list(set(self_data_in_right + self_data_with_index_in_left))) data_need_return = self_data_without_index_in_left @@ -517,9 +548,10 @@ def class2func(cls_func, host, func_name=None, show_code=False): code_scope[host_name] = host # codes - main_code = f'def new_{func_name}({", ".join(new_args)}):\n' + main_code + header = f'def new_{func_name}({", ".join(new_args)}):\n' + main_code = header + tools.indent(main_code, spaces_per_tab=2) if len(returns): - main_code += f'\n{" " * num_indent}return {", ".join(returns)}' + main_code += f'\n{" " * num_indent + " "}return {", ".join(returns)}' main_code = tools.word_replace(main_code, replaces_later) if show_code: print(main_code) diff --git a/brainpy/simulation/dynamic_system.py b/brainpy/simulation/dynamic_system.py index a7099047..53c07f12 100644 --- a/brainpy/simulation/dynamic_system.py +++ b/brainpy/simulation/dynamic_system.py @@ -11,7 +11,7 @@ __all__ = [ 'DynamicSystem', ] -_DynamicModel_NO = 0 +_DynamicSystem_NO = 0 class DynamicSystem(object): @@ -19,12 +19,16 @@ class DynamicSystem(object): Parameters ---------- - name : str - The name of the dynamic system. steps : callable, list of callable, dict The callable function, or a list of callable functions. monitors : list, tuple, None Variables to monitor. + name : str + The name of the dynamic system. + host : any + The host to store data, including variables, functions, etc. + show_code : bool + Whether show the formatted codes. """ target_backend = None @@ -51,9 +55,9 @@ class DynamicSystem(object): # name # ---- if name is None: - global _DynamicModel_NO - name = f'DM{_DynamicModel_NO}' - _DynamicModel_NO += 1 + global _DynamicSystem_NO + name = f'DS{_DynamicSystem_NO}' + _DynamicSystem_NO += 1 if not name.isidentifier(): raise errors.ModelUseError(f'"{name}" isn\'t a valid identifier according to Python ' f'language definition. Please choose another name.') diff --git a/docs/advanced/HH_model_in_ANNarchy.ipynb b/docs/advanced/HH_model_in_ANNarchy.ipynb deleted file mode 100644 index bea7d882..00000000 --- a/docs/advanced/HH_model_in_ANNarchy.ipynb +++ /dev/null @@ -1,291 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# HH model in ANNarchy" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When I first encounter [ANNarchy](https://annarchy.readthedocs.io), I am \n", - "exciting and feel happy, because ANNarchy is much more easy-to-use than [Brian2](https://brian2.readthedocs.io/).\n", - "However, one day I want to implement a complex Hodgkin-Huxley-like conductance neuron model,\n", - "I tried my best and cannot get what I want. \n", - "\n", - "I gradually knew this is the bug of ANNarchy for Hodgkin-Huxley implementation. \n", - "And more sadly, although I knew it is wrong, I cannot fix it. All the things seem to be right,\n", - "but I don't know what's wrong. \n", - "\n", - "In the next, I will show the bugs of `Hodgkin-Huxley model` provided by ANNarchy official \n", - "[examples](https://annarchy.readthedocs.io/en/latest/example/HodgkinHuxley.html)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First, let's import the library, and define the visualzation functions." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ANNarchy 4.6 (4.6.8.1) on linux (posix). \n" - ] - } - ], - "source": [ - "import sys\n", - "from ANNarchy import *\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "def show_mon(ts, mon):\n", - " data = mon.get()\n", - " plt.subplot(2, 2, 1)\n", - " plt.plot(ts, data['V'][:, 0])\n", - " plt.title('V')\n", - " plt.subplot(2, 2, 2)\n", - " plt.plot(ts, data['n'][:, 0])\n", - " plt.title('n')\n", - " plt.ylim((0.0, 1.0))\n", - " plt.subplot(2, 2, 3)\n", - " plt.plot(ts, data['m'][:, 0])\n", - " plt.title('m')\n", - " plt.ylim((0.0, 1.0))\n", - " plt.subplot(2, 2, 4)\n", - " plt.plot(ts, data['h'][:, 0])\n", - " plt.title('h')\n", - " plt.ylim((0.0, 1.0))\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "lines_to_next_cell": 2 - }, - "source": [ - "Then, let's define the running function of the model." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def run_model(Iext):\n", - " dt = 0.01\n", - " setup(dt=dt)\n", - "\n", - " HH = Neuron(\n", - " parameters=\"\"\"\n", - " C = 1.0 # Capacitance\n", - " VL = -59.387 # Leak voltage\n", - " VK = -82.0 # Potassium reversal voltage\n", - " VNa = 45.0 # Sodium reveral voltage\n", - " gK = 36.0 # Maximal Potassium conductance\n", - " gNa = 120.0 # Maximal Sodium conductance\n", - " gL = 0.3 # Leak conductance\n", - " vt = 30.0 # Threshold for spike emission\n", - " I = 0.0 # External current\n", - " \"\"\",\n", - "\n", - " equations=\"\"\"\n", - " # Previous membrane potential\n", - " prev_V = V\n", - "\n", - " # Voltage-dependency parameters\n", - " an = 0.01 * (V + 60.0) / (1.0 - exp(-0.1* (V + 60.0) ) )\n", - " am = 0.1 * (V + 45.0) / (1.0 - exp (- 0.1 * ( V + 45.0 )))\n", - " ah = 0.07 * exp(- 0.05 * ( V + 70.0 ))\n", - "\n", - " bn = 0.125 * exp (- 0.0125 * (V + 70.0))\n", - " bm = 4.0 * exp (- (V + 70.0) / 80.0)\n", - " bh = 1.0/(1.0 + exp (- 0.1 * ( V + 40.0 )) )\n", - "\n", - " # Alpha/Beta functions\n", - " dn/dt = an * (1.0 - n) - bn * n : init = 0.3, midpoint\n", - " dm/dt = am * (1.0 - m) - bm * m : init = 0.0, midpoint\n", - " dh/dt = ah * (1.0 - h) - bh * h : init = 0.6, midpoint\n", - "\n", - " # Membrane equation\n", - " C * dV/dt = gL * (VL - V ) + gK * n**4 * (VK - V) + gNa * m**3 * h * (VNa - V) + I : midpoint\n", - "\n", - " \"\"\",\n", - "\n", - " spike=\"\"\"\n", - " # Spike is emitted when the membrane potential crosses the threshold from below\n", - " (V > vt) and (prev_V <= vt) \n", - " \"\"\",\n", - "\n", - " reset=\"\"\"\n", - " # Nothing to do, it is built-in...\n", - " \"\"\"\n", - " )\n", - "\n", - " pop = Population(neuron=HH, geometry=1)\n", - " pop.V = -50.0\n", - "\n", - " compile(clean=True)\n", - "\n", - " m = Monitor(pop, ['spike', 'V', 'n', 'm', 'h'])\n", - "\n", - " duration = sum([d for _, d in Iext])\n", - "\n", - " for I, dur in Iext:\n", - " pop.I = I\n", - " simulate(dur)\n", - "\n", - " ts = np.arange(0, duration, dt)\n", - " show_mon(ts, m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The official example provide the external inputs like this:\n", - "- The first 100 ms, warm up the model;\n", - "- The next 1 ms, provide the curren `I=200.0`;\n", - "- Final 100 ms, no external inputs.\n", - "\n", - "The results are shown as in the following." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "WARNING: unrecognized arguments: ['-f', '/home/chaoming/.local/share/jupyter/runtime/kernel-40129879-e6c5-4d19-ad25-104c13d99d87.json'] \n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "run_model([(0., 100.), (200., 1.), (0., 100.)])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, when we sightly change the external inputs. For example, provide\n", - "the current `I=200.0` with the length of `101 ms`. We would expect that the model\n", - "will produce repetive firing. However, the actual running results" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "WARNING: unrecognized arguments: ['-f', '/home/chaoming/.local/share/jupyter/runtime/kernel-40129879-e6c5-4d19-ad25-104c13d99d87.json'] \n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "run_model([(0., 100.), (200., 101.)])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "So, how can I fix the wrong scripts? \n", - "\n", - "All the things seems right, what is wrong?\n", - "\n", - "How can I/you do?" - ] - } - ], - "metadata": { - "hide_input": false, - "jupytext": { - "cell_metadata_filter": "-all", - "notebook_metadata_filter": "-all" - }, - "kernelspec": { - "display_name": "py37", - "language": "python", - "name": "py37" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": false, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": true - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/advanced/gapjunction_lif_in_brian2.ipynb b/docs/advanced/gapjunction_lif_in_brian2.ipynb deleted file mode 100644 index 5c2202ee..00000000 --- a/docs/advanced/gapjunction_lif_in_brian2.ipynb +++ /dev/null @@ -1,266 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Gap junction model between LIF neurons" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:19:38.078180Z", - "start_time": "2020-07-03T11:19:35.116911Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2.3\n" - ] - } - ], - "source": [ - "from brian2 import *\n", - "from brian2 import __version__\n", - "print(__version__)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:19:38.872103Z", - "start_time": "2020-07-03T11:19:38.859131Z" - } - }, - "outputs": [], - "source": [ - "seed(12345)\n", - "prefs.codegen.target = \"numpy\"" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:19:40.056821Z", - "start_time": "2020-07-03T11:19:39.563021Z" - } - }, - "outputs": [], - "source": [ - "import matplotlib.patches as patches\n", - "import npbrain.all as nn" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:19:40.570117Z", - "start_time": "2020-07-03T11:19:40.563117Z" - } - }, - "outputs": [], - "source": [ - "gj_w, k_spikelet = 1., 0.5\n", - "size = (10, 10)\n", - "Vr = 0.\n", - "Vth = 10.\n", - "tau = 10 * ms\n", - "Iext = 12.\n", - "noise = 1.\n", - "duration = 500" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:19:42.962321Z", - "start_time": "2020-07-03T11:19:42.917160Z" - } - }, - "outputs": [], - "source": [ - "eqs = '''\n", - " dV/dt = (-V + Vr + Igap + Iext + sqrt(1*ms) * noise * xi) / tau : 1\n", - " Igap : 1 # gap junction current\n", - "'''\n", - "neurons = NeuronGroup(size[0] * size[1], eqs, threshold='V>Vth', reset='V=Vr', method='euler')\n", - "neurons.V = 'rand() * (Vth - Vr) + Vr'" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:19:43.584493Z", - "start_time": "2020-07-03T11:19:43.546195Z" - } - }, - "outputs": [], - "source": [ - "S = Synapses(source=neurons,\n", - " target=neurons,\n", - " model='''w : 1 # gap junction conductance\n", - " Igap_post = w * (V_pre - V_post) : 1 (summed)''',\n", - " on_pre='V_post += w * {}'.format(k_spikelet))\n", - "pre_index, post_index, _ = nn.conn.grid_four(size[0], size[1])\n", - "S.connect(i=pre_index, j=post_index)\n", - "S.w = gj_w" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:19:46.878301Z", - "start_time": "2020-07-03T11:19:46.206244Z" - } - }, - "outputs": [], - "source": [ - "mon_st = StateMonitor(neurons, 'V', record=True)\n", - "mon_sp = SpikeMonitor(neurons, record=True)\n", - "run(duration * ms)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:20:20.435342Z", - "start_time": "2020-07-03T11:20:20.421348Z" - } - }, - "outputs": [], - "source": [ - "neuron_indexes = [1]\n", - "spike_trains = mon_sp.spike_trains()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "ExecuteTime": { - "end_time": "2020-07-03T11:20:21.679284Z", - "start_time": "2020-07-03T11:20:21.454281Z" - } - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "fig, gs = nn.vis.get_figure(1, 1, 6, 14)\n", - "ax = fig.add_subplot(gs[0, 0])\n", - "ax.plot([0, duration], [Vth, Vth], 'k', label='threshold')\n", - "for i in neuron_indexes:\n", - " ax.plot(mon_st.t / ms, mon_st.V[i], label='N{}-potential'.format(i))\n", - " spikes = spike_trains[1] / ms\n", - " ax.plot(spikes, np.ones_like(spikes) * Vth, '.r', markersize=10, label='N{}-spikes'.format(i))\n", - " ax.add_patch(patches.Rectangle((133, 9.8), 8, 0.4, linewidth=1, edgecolor='r', facecolor='none'))\n", - " ax.add_patch(patches.Rectangle((170, 9.8), 8, 0.4, linewidth=1, edgecolor='r', facecolor='none'))\n", - " ax.add_patch(patches.Rectangle((293, 9.8), 8, 0.4, linewidth=1, edgecolor='r', facecolor='none'))\n", - "ax.set_xlabel('Time (ms)')\n", - "ax.set_ylabel('Potential (mV)')\n", - "ax.legend(loc='lower center', fontsize=14)\n", - "xlim(99, 351)\n", - "ylim(-0.5, 12.)\n", - "show()" - ] - } - ], - "metadata": { - "hide_input": false, - "jupytext": { - "formats": "py:light,ipynb" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": false, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": true - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/neurons/LIF_model.py b/examples/neurons/LIF_model.py new file mode 100644 index 00000000..fa808ef2 --- /dev/null +++ b/examples/neurons/LIF_model.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + + +import numba as nb + +import brainpy as bp + + +class LIF(bp.NeuGroup): + def __init__(self, size, t_refractory=1., V_rest=0., + V_reset=-5., V_th=20., R=1., tau=10., **kwargs): + # parameters + self.V_rest = V_rest + self.V_reset = V_reset + self.V_th = V_th + self.R = R + self.tau = tau + self.t_refractory = t_refractory + + # variables + self.t_last_spike = bp.backend.ones(size) * -1e7 + self.refractory = bp.backend.zeros(size) + self.input = bp.backend.zeros(size) + self.spike = bp.backend.zeros(size) + self.V = bp.backend.ones(size) * V_reset + + super(LIF, self).__init__(size=size, **kwargs) + + @staticmethod + @bp.odeint + def int_V(V, t, Iext, V_rest, R, tau): + return (- (V - V_rest) + R * Iext) / tau + + def update(self, _t): + for i in nb.prange(self.size[0]): + if _t - self.t_last_spike[i] <= self.t_refractory: + self.refractory[i] = 1. + else: + self.refractory[0] = 0. + V = self.int_V(self.V[i], _t, self.input[i], self.V_rest, self.R, self.tau) + if V >= self.V_th: + self.V[i] = self.V_reset + self.spike[i] = 1. + self.t_last_spike[i] = _t + else: + self.spike[i] = 0. + self.V[i] = V + self.input[i] = 0. diff --git a/examples/synapses/AMPA_synapse.py b/examples/synapses/AMPA_synapse.py index b78d4911..9e5edb34 100644 --- a/examples/synapses/AMPA_synapse.py +++ b/examples/synapses/AMPA_synapse.py @@ -100,7 +100,7 @@ class AMPA1_vec(bp.TwoEndConn): pre_id = self.pre_ids[i] self.s[i] = self.int_s(self.s[i], _t, self.tau) self.s[i] += self.pre.spike[pre_id] - self.g.push(i, self.g_max * self.s[i]) + self.g.push(i,self.g_max * self.s[i]) post_id = self.post_ids[i] self.post.input[post_id] -= self.g.pull(i) * (self.post.V[post_id] - self.E) @@ -137,7 +137,7 @@ class AMPA1_mat(bp.TwoEndConn): if self.pre.spike[i] > 0: self.s[i] += self.conn_mat[i] self.g.push(self.g_max * self.s) - g = self.g.pull() + g=self.g.pull() self.post.input -= bp.backend.sum(g, axis=0) * (self.post.V - self.E) diff --git a/tests/backend/runners/numba_cpu_runner.py b/tests/backend/runners/numba_cpu_runner.py index 41b47ba9..977ba17f 100644 --- a/tests/backend/runners/numba_cpu_runner.py +++ b/tests/backend/runners/numba_cpu_runner.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- +import ast import inspect -from brainpy.backend.runners.numba_cpu_runner import analyze_step_func -from brainpy.backend.runners.numba_cpu_runner import StepFuncReader - from pprint import pprint -import ast + import numpy as np + import brainpy as bp +from brainpy.backend.runners.numba_cpu_runner import StepFuncReader +from brainpy.backend.runners.numba_cpu_runner import analyze_step_func def test_analyze_step1(): @@ -141,7 +142,6 @@ def test_analyze_step2(): self.n[:] = n self.input[:] = 0. - group = HH(100, ['V']) r = analyze_step_func(group.update) @@ -180,7 +180,7 @@ def test_StepFuncReader1(): self.spike = np.zeros(size) self.input = np.zeros(size) - super(HH, self).__init__(size=size, steps=[self.update], **kwargs) + super(HH, self).__init__(size=size, **kwargs) @staticmethod @bp.odeint @@ -234,8 +234,7 @@ def test_StepFuncReader1(): self.s = bp.backend.zeros(self.size) self.g = self.register_constant_delay('g', size=self.size, delay_time=delay) - super(AMPA1_vec, self).__init__(steps=[self.update, ], - pre=pre, post=post, **kwargs) + super(AMPA1_vec, self).__init__(pre=pre, post=post, **kwargs) @staticmethod @bp.odeint(method='euler') @@ -275,6 +274,67 @@ def test_StepFuncReader1(): print() -test_StepFuncReader1() +def test_StepFuncReader2(): + class LIF(bp.NeuGroup): + target_backend = ['numba', 'numpy'] + def __init__(self, size, t_refractory=1., V_rest=0., + V_reset=-5., V_th=20., R=1., tau=10., **kwargs): + # parameters + self.V_rest = V_rest + self.V_reset = V_reset + self.V_th = V_th + self.R = R + self.tau = tau + self.t_refractory = t_refractory + + # variables + self.t_last_spike = bp.backend.ones(size) * -1e7 + self.refractory = bp.backend.zeros(size) + self.input = bp.backend.zeros(size) + self.spike = bp.backend.zeros(size) + self.V = bp.backend.ones(size) * V_reset + + super(LIF, self).__init__(size=size, **kwargs) + + @staticmethod + @bp.odeint + def int_V(V, t, Iext, V_rest, R, tau): + return (- (V - V_rest) + R * Iext) / tau + + def update(self, _t): + for i in range(self.size[0]): + if _t - self.t_last_spike[i] <= self.t_refractory: + self.refractory[i] = 1. + else: + self.refractory[0] = 0. + V = self.int_V(self.V[i], _t, self.input[i], self.V_rest, self.R, self.tau) + if V >= self.V_th: + self.V[i] = self.V_reset + self.spike[i] = 1. + self.t_last_spike[i] = _t + else: + self.spike[i] = 0. + self.V[i] = V + self.input[i] = 0. + + lif = LIF(10) + code = bp.tools.deindent(inspect.getsource(lif.update)) + + formatter = StepFuncReader(host=lif) + formatter.visit(ast.parse(code)) + + print('lefts:') + pprint(formatter.lefts) + print() + print('rights:') + pprint(formatter.rights) + print() + print('lines:') + pprint(formatter.lines) + print() + print('delay_call:') + pprint(formatter.delay_call) + print() +test_StepFuncReader1() -- 2.34.1 From 39005d8495b98003b025bf393161b2dd63fdcca1 Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Mon, 22 Mar 2021 23:52:10 +0800 Subject: [PATCH 08/15] Improve NumbaCPUNodeRunner --- brainpy/backend/runners/numba_cpu_runner.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/brainpy/backend/runners/numba_cpu_runner.py b/brainpy/backend/runners/numba_cpu_runner.py index f5692a29..ba583ae9 100644 --- a/brainpy/backend/runners/numba_cpu_runner.py +++ b/brainpy/backend/runners/numba_cpu_runner.py @@ -3,10 +3,8 @@ import ast import inspect import re -from collections import OrderedDict import numba -from numba.core.dispatcher import Dispatcher from brainpy import backend from brainpy import errors -- 2.34.1 From bc35b8c738bb1d1474b6af65024360486abb0a9e Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Tue, 23 Mar 2021 12:34:40 +0800 Subject: [PATCH 09/15] Fix "data_need_pass" error --- brainpy/backend/runners/numba_cpu_runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brainpy/backend/runners/numba_cpu_runner.py b/brainpy/backend/runners/numba_cpu_runner.py index ba583ae9..ced86fe2 100644 --- a/brainpy/backend/runners/numba_cpu_runner.py +++ b/brainpy/backend/runners/numba_cpu_runner.py @@ -344,7 +344,8 @@ def analyze_step_func(host, f): class_p1 = '\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*\\b' self_data_without_index_in_left = set(re.findall(class_p1, code)) class_p2 = '(\\b' + args[0] + '\\.[A-Za-z_][A-Za-z0-9_.]*)\\[.*\\]' - self_data_with_index_in_left = set(re.findall(class_p2, code)) - self_data_without_index_in_left + self_data_with_index_in_left = set(re.findall(class_p2, code)) #- self_data_without_index_in_left + # self_data_with_index_in_left = set(re.findall(class_p2, code)) - self_data_without_index_in_left self_data_with_index_in_left = list(self_data_with_index_in_left) self_data_without_index_in_left = list(self_data_without_index_in_left) -- 2.34.1 From 0c032d660e62458cc182709feb74274ff494c03e Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Tue, 23 Mar 2021 13:42:48 +0800 Subject: [PATCH 10/15] Support Trajectory in PhasePlane and Bifurcation analysis --- brainpy/analysis/base.py | 6 +- brainpy/analysis/bifurcation.py | 90 +++------ .../{dyn_model.py => integrals2model.py} | 17 +- brainpy/analysis/phase_plane.py | 176 +++++++++--------- brainpy/analysis/trajectory.py | 109 +++++++++++ examples/neurons/FitzHugh_Nagumo.py | 14 +- examples/neurons/LIF_model.py | 14 ++ 7 files changed, 260 insertions(+), 166 deletions(-) rename brainpy/analysis/{dyn_model.py => integrals2model.py} (80%) create mode 100644 brainpy/analysis/trajectory.py diff --git a/brainpy/analysis/base.py b/brainpy/analysis/base.py index 4259f73b..73e83d5f 100644 --- a/brainpy/analysis/base.py +++ b/brainpy/analysis/base.py @@ -8,7 +8,7 @@ import sympy from brainpy import errors from brainpy import tools -from brainpy.analysis import dyn_model +from brainpy.analysis import integrals2model from brainpy.analysis import solver from brainpy.analysis import utils from brainpy.integrators import sympy_analysis @@ -82,11 +82,11 @@ class BaseNeuronAnalyzer(object): # model # ----- - if isinstance(model_or_integrals, dyn_model.DynamicModel): + if isinstance(model_or_integrals, integrals2model.DynamicModel): self.model = model_or_integrals elif (isinstance(model_or_integrals, (tuple, list)) and callable(model_or_integrals[0])) or \ callable(model_or_integrals): - self.model = dyn_model.transform_integrals_to_analyzers(model_or_integrals) + self.model = integrals2model.transform_integrals_to_model(model_or_integrals) else: raise ValueError diff --git a/brainpy/analysis/bifurcation.py b/brainpy/analysis/bifurcation.py index 7cb8927d..892fcf99 100644 --- a/brainpy/analysis/bifurcation.py +++ b/brainpy/analysis/bifurcation.py @@ -9,11 +9,11 @@ from mpl_toolkits.mplot3d import Axes3D from brainpy import backend from brainpy import errors -from brainpy import simulation from brainpy.analysis import base -from brainpy.analysis import dyn_model +from brainpy.analysis import integrals2model from brainpy.analysis import stability from brainpy.analysis import utils +from brainpy.analysis.trajectory import Trajectory __all__ = [ 'Bifurcation', @@ -48,7 +48,7 @@ class Bifurcation(object): def __init__(self, integrals, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): # check "model" - self.model = dyn_model.transform_integrals_to_analyzers(integrals) + self.model = integrals2model.transform_integrals_to_model(integrals) # check "target_pars" if not isinstance(target_pars, dict): @@ -372,23 +372,13 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): # initialize neuron group length = all_xs.shape[0] - group = simulation.NeuGroup(self.model, - size=length, - monitors=self.dvar_names, - pars_update=self.pars_update) - - # group initial state - group.ST[self.x_var] = all_xs - group.ST[self.y_var] = all_ys - for key, val in fixed_vars.items(): - if key in group.ST: - group.ST[key] = val - - # run neuron group - group.runner = simulation.TrajectoryNumbaRunner(group, - target_vars=self.dvar_names, - fixed_vars=fixed_vars) - group.run(duration=duration, inputs=inputs) + traj_group = Trajectory(size=length, + integrals=self.model.integrals, + target_vars={self.x_var: all_xs, self.y_var: all_ys}, + fixed_vars=fixed_vars, + pars_update=self.pars_update, + scope=self.model.scopes) + traj_group.run(duration=duration) # find limit cycles limit_cycle_max = [] @@ -397,7 +387,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): p0_limit_cycle = [] p1_limit_cycle = [] for i in range(length): - data = group.mon[var][:, i] + data = traj_group.mon[var][:, i] max_index = utils.find_indexes_of_limit_cycle_max(data, tol=tol) if max_index[0] != -1: x_cycle = data[max_index[0]: max_index[1]] @@ -433,9 +423,7 @@ class _Bifurcation2D(base.Base2DNeuronAnalyzer): if show: plt.show() - del group.ST - del group.mon - del group + del traj_group gc.collect() @@ -466,7 +454,7 @@ class FastSlowBifurcation(object): def __init__(self, integrals, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): # check "model" - self.model = dyn_model.transform_integrals_to_analyzers(integrals) + self.model = integrals2model.transform_integrals_to_model(integrals) # check "fast_vars" if not isinstance(fast_vars, dict): @@ -542,10 +530,10 @@ class FastSlowBifurcation(object): class _FastSlowTrajectory(object): def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, **kwargs): - if isinstance(model_or_intgs, dyn_model.DynamicModel): + if isinstance(model_or_intgs, integrals2model.DynamicModel): self.model = model_or_intgs elif (isinstance(model_or_intgs, (list, tuple)) and callable(model_or_intgs[0])) or callable(model_or_intgs): - self.model = dyn_model.transform_integrals_to_analyzers(model_or_intgs) + self.model = integrals2model.transform_integrals_to_model(model_or_intgs) else: raise ValueError self.fast_vars = fast_vars @@ -569,20 +557,6 @@ class _FastSlowTrajectory(object): else: self.slow_var_names = list(sorted(slow_vars.keys())) - # TODO - # cannot update dynamical parameters - all_vars = self.fast_var_names + self.slow_var_names - self.traj_group = simulation.NeuGroup(model_or_intgs, - size=1, - monitors=all_vars, - pars_update=pars_update) - self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, - target_vars=all_vars, - fixed_vars=fixed_vars) - self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() - if not key.startswith('_')} - self.traj_net = simulation.Network(self.traj_group) - def plot_trajectory(self, initials, duration, plot_duration=None, inputs=(), show=False): """Plot trajectories according to the settings. @@ -648,29 +622,15 @@ class _FastSlowTrajectory(object): else: assert len(plot_duration) == len(initials) - # 4. format the inputs - if len(inputs): - if isinstance(inputs[0], (tuple, list)): - inputs = [(self.traj_group,) + tuple(input) for input in inputs] - elif isinstance(inputs[0], str): - inputs = [(self.traj_group,) + tuple(inputs)] - else: - raise errors.ModelUseError() - # 5. run the network for init_i, initial in enumerate(initials): - # 5.1 set the initial value - for key, val in self.traj_initial.items(): - self.traj_group.ST[key] = val - for key in all_vars: - self.traj_group.ST[key] = initial[key] - for key, val in self.fixed_vars.items(): - if key in self.traj_group.ST: - self.traj_group.ST[key] = val - - # 5.2 run the model - self.traj_net.run(duration=duration[init_i], inputs=inputs, - report=False, data_to_host=True, verbose=False) + traj_group = Trajectory(size=1, + integrals=self.model.integrals, + target_vars=initial, + fixed_vars=self.fixed_vars, + pars_update=self.pars_update, + scope=self.model.scopes) + traj_group.run(duration=duration, report=False) # 5.3 legend legend = f'$traj_{init_i}$: ' @@ -684,8 +644,8 @@ class _FastSlowTrajectory(object): # 5.5 visualization for var_name in self.fast_var_names: - s0 = self.traj_group.mon[self.slow_var_names[0]][start: end, 0] - fast = self.traj_group.mon[var_name][start: end, 0] + s0 = traj_group.mon[self.slow_var_names[0]][start: end, 0] + fast = traj_group.mon[var_name][start: end, 0] fig = plt.figure(var_name) if len(self.slow_var_names) == 1: @@ -698,7 +658,7 @@ class _FastSlowTrajectory(object): elif len(self.slow_var_names) == 2: fig.gca(projection='3d') - s1 = self.traj_group.mon[self.slow_var_names[1]][start: end, 0] + s1 = traj_group.mon[self.slow_var_names[1]][start: end, 0] plt.plot(s0, s1, fast, label=legend) else: raise errors.AnalyzerError diff --git a/brainpy/analysis/dyn_model.py b/brainpy/analysis/integrals2model.py similarity index 80% rename from brainpy/analysis/dyn_model.py rename to brainpy/analysis/integrals2model.py index d18af375..d4c05fe8 100644 --- a/brainpy/analysis/dyn_model.py +++ b/brainpy/analysis/integrals2model.py @@ -3,6 +3,7 @@ import inspect +from brainpy import errors from brainpy.integrators import ast_analysis from brainpy.integrators import sympy_analysis @@ -12,12 +13,12 @@ except ModuleNotFoundError: Dispatcher = None __all__ = [ - 'transform_integrals_to_analyzers', + 'transform_integrals_to_model', 'DynamicModel', ] -def transform_integrals_to_analyzers(integrals): +def transform_integrals_to_model(integrals): if callable(integrals): integrals = [integrals] @@ -64,18 +65,24 @@ def transform_integrals_to_analyzers(integrals): analyzers.append(DE) # others - all_variables.update(integral.variables) + for var in integral.variables: + if var in all_variables: + raise errors.ModelDefError(f'Variable {var} has been defined before. Cannot group ' + f'this integral as a dynamic system.') + all_variables.add(var) all_parameters.update(integral.parameters) all_scope.update(code_scope) - return DynamicModel(analyzers=analyzers, + return DynamicModel(integrals=integrals, + analyzers=analyzers, variables=list(all_variables), parameters=list(all_parameters), scopes=all_scope) class DynamicModel(object): - def __init__(self, analyzers, variables, parameters, scopes): + def __init__(self, integrals, analyzers, variables, parameters, scopes): + self.integrals = integrals self.analyzers = analyzers self.variables = variables self.parameters = parameters diff --git a/brainpy/analysis/phase_plane.py b/brainpy/analysis/phase_plane.py index 4b5b4973..1654e6c9 100644 --- a/brainpy/analysis/phase_plane.py +++ b/brainpy/analysis/phase_plane.py @@ -5,11 +5,11 @@ import numpy as np from brainpy import backend from brainpy import errors -from brainpy import simulation from brainpy.analysis import base -from brainpy.analysis import dyn_model +from brainpy.analysis import integrals2model from brainpy.analysis import stability from brainpy.analysis import utils +from brainpy.analysis.trajectory import Trajectory __all__ = [ 'PhasePlane', @@ -86,7 +86,7 @@ class PhasePlane(object): options=None, ): # check "model" - self.model = dyn_model.transform_integrals_to_analyzers(integrals) + self.model = integrals2model.transform_integrals_to_model(integrals) # check "target_vars" if not isinstance(target_vars, dict): @@ -145,22 +145,73 @@ class PhasePlane(object): """Plot nullcline (only supported in 2D system).""" self.analyzer.plot_nullcline(*args, **kwargs) - def plot_trajectory(self, *args, **kwargs): - """Plot trajectories (only supported in 2D system).""" - self.analyzer.plot_trajectory(*args, **kwargs) + def plot_trajectory(self, initials, duration, plot_duration=None, axes='v-v', show=False): + """Plot trajectories according to the settings. - def plot_limit_cycle_by_sim(self, *args, **kwargs): - """Find the limit cycles through the simulation, and then plot.""" - self.analyzer.plot_limit_cycle_by_sim(*args, **kwargs) + Parameters + ---------- + initials : list, tuple, dict + The initial value setting of the targets. It can be a tuple/list of floats to specify + each value of dynamical variables (for example, ``(a, b)``). It can also be a + tuple/list of tuple to specify multiple initial values (for example, + ``[(a1, b1), (a2, b2)]``). + duration : int, float, tuple, list + The running duration. Same with the ``duration`` in ``NeuGroup.run()``. + It can be a int/float (``t_end``) to specify the same running end time, + or it can be a tuple/list of int/float (``(t_start, t_end)``) to specify + the start and end simulation time. Or, it can be a list of tuple + (``[(t1_start, t1_end), (t2_start, t2_end)]``) to specify the specific + start and end simulation time for each initial value. + plot_duration : tuple, list, optional + The duration to plot. It can be a tuple with ``(start, end)``. It can + also be a list of tuple ``[(start1, end1), (start2, end2)]`` to specify + the plot duration for each initial value running. + axes : str + The axes to plot. It can be: + + - 'v-v' + Plot the trajectory in the 'x_var'-'y_var' axis. + - 't-v' + Plot the trajectory in the 'time'-'var' axis. + show : bool + Whether show or not. + """ + self.analyzer.plot_trajectory(initials=initials, + duration=duration, + plot_duration=plot_duration, + axes=axes, + show=show) + + def plot_limit_cycle_by_sim(self, initials, duration, tol=0.001, show=False): + """Plot limit cycles according to the settings. + + Parameters + ---------- + initials : list, tuple + The initial value setting of the targets. It can be a tuple/list of floats to specify + each value of dynamical variables (for example, ``(a, b)``). It can also be a + tuple/list of tuple to specify multiple initial values (for example, + ``[(a1, b1), (a2, b2)]``). + duration : int, float, tuple, list + The running duration. Same with the ``duration`` in ``NeuGroup.run()``. + It can be a int/float (``t_end``) to specify the same running end time, + or it can be a tuple/list of int/float (``(t_start, t_end)``) to specify + the start and end simulation time. Or, it can be a list of tuple + (``[(t1_start, t1_end), (t2_start, t2_end)]``) to specify the specific + start and end simulation time for each initial value. + show : bool + Whether show or not. + """ + self.analyzer.plot_limit_cycle_by_sim(initials=initials, + duration=duration, + tol=tol, + show=show) class _PhasePlane1D(base.Base1DNeuronAnalyzer): """Phase plane analyzer for 1D system. """ - def __init__(self, *args, **kwargs): - super(_PhasePlane1D, self).__init__(*args, **kwargs) - def plot_vector_field(self, show=False): """Plot the vector filed. @@ -191,7 +242,7 @@ class _PhasePlane1D(base.Base1DNeuronAnalyzer): plt.xlabel(self.x_var) plt.ylabel(label) - plt.xlim(*utils.rescale(self.target_vars[self.x_var], + plt.xlim(*utils.rescale(self.target_vars[self.x_var], scale=(self.options.lim_scale - 1.) / 2)) plt.legend() if show: @@ -253,26 +304,6 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): """Phase plane analyzer for 2D system. """ - def __init__(self, *args, **kwargs): - super(_PhasePlane2D, self).__init__(*args, **kwargs) - - - # TODO - # # runner for trajectory - # # --------------------- - # - # # cannot update dynamical parameters - # self.traj_group = simulation.NeuGroup(self.model, - # size=1, - # monitors=self.dvar_names, - # pars_update=self.pars_update) - # self.traj_group.runner = simulation.TrajectoryNumbaRunner(self.traj_group, - # target_vars=self.dvar_names, - # fixed_vars=self.fixed_vars) - # self.traj_initial = {key: val[0] for key, val in self.traj_group.ST.items() - # if not key.startswith('_')} - # self.traj_net = simulation.Network(self.traj_group) - def plot_vector_field(self, plot_method='streamplot', plot_style=None, show=False): """Plot the vector field. @@ -516,12 +547,12 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): return {self.x_eq_group.func_name: (x_values_in_x_eq, y_values_in_x_eq), self.y_eq_group.func_name: (x_values_in_y_eq, y_values_in_y_eq)} - def plot_trajectory(self, initials, duration, plot_duration=None, inputs=(), axes='v-v', show=False): + def plot_trajectory(self, initials, duration, plot_duration=None, axes='v-v', show=False): """Plot trajectories according to the settings. Parameters ---------- - initials : list, tuple + initials : list, tuple, dict The initial value setting of the targets. It can be a tuple/list of floats to specify each value of dynamical variables (for example, ``(a, b)``). It can also be a tuple/list of tuple to specify multiple initial values (for example, @@ -537,8 +568,6 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): The duration to plot. It can be a tuple with ``(start, end)``. It can also be a list of tuple ``[(start1, end1), (start2, end2)]`` to specify the plot duration for each initial value running. - inputs : tuple, list - The inputs to the model. Same with the ``inputs`` in ``NeuGroup.run()`` axes : str The axes to plot. It can be: @@ -586,30 +615,17 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): else: assert len(plot_duration) == len(initials) - # 4. format the inputs - if len(inputs): - if isinstance(inputs[0], (tuple, list)): - inputs = [(self.traj_group,) + tuple(input) - for input in inputs] - elif isinstance(inputs[0], str): - inputs = [(self.traj_group,) + tuple(inputs)] - else: - raise errors.ModelUseError() - # 5. run the network for init_i, initial in enumerate(initials): - # 5.1 set the initial value - for key, val in self.traj_initial.items(): - self.traj_group.ST[key] = val - for key in self.dvar_names: - self.traj_group.ST[key] = initial[key] - for key, val in self.fixed_vars.items(): - if key in self.traj_group.ST: - self.traj_group.ST[key] = val + traj_group = Trajectory(size=1, + integrals=self.model.integrals, + target_vars=initial, + fixed_vars=self.fixed_vars, + pars_update=self.pars_update, + scope=self.model.scopes) # 5.2 run the model - self.traj_net.run(duration=duration[init_i], inputs=inputs, - report=False, data_to_host=True, verbose=False) + traj_group.run(duration=duration[init_i], report=False, ) # 5.3 legend legend = f'$traj_{init_i}$: ' @@ -623,16 +639,16 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): # 5.5 visualization if axes == 'v-v': - lines = plt.plot(self.traj_group.mon[self.x_var][start: end, 0], - self.traj_group.mon[self.y_var][start: end, 0], + lines = plt.plot(traj_group.mon[self.x_var][start: end, 0], + traj_group.mon[self.y_var][start: end, 0], label=legend) utils.add_arrow(lines[0]) else: - plt.plot(self.traj_group.mon.ts[start: end], - self.traj_group.mon[self.x_var][start: end, 0], + plt.plot(traj_group.mon.ts[start: end], + traj_group.mon[self.x_var][start: end, 0], label=legend + f', {self.x_var}') - plt.plot(self.traj_group.mon.ts[start: end], - self.traj_group.mon[self.y_var][start: end, 0], + plt.plot(traj_group.mon.ts[start: end], + traj_group.mon[self.y_var][start: end, 0], label=legend + f', {self.y_var}') # 6. visualization @@ -649,7 +665,7 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): if show: plt.show() - def plot_limit_cycle_by_sim(self, initials, duration, inputs=(), tol=0.001, show=False): + def plot_limit_cycle_by_sim(self, initials, duration, tol=0.001, show=False): """Plot trajectories according to the settings. Parameters @@ -666,8 +682,6 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): the start and end simulation time. Or, it can be a list of tuple (``[(t1_start, t1_end), (t2_start, t2_end)]``) to specify the specific start and end simulation time for each initial value. - inputs : tuple, list - The inputs to the model. Same with the ``inputs`` in ``NeuGroup.run()`` show : bool Whether show or not. """ @@ -696,31 +710,19 @@ class _PhasePlane2D(base.Base2DNeuronAnalyzer): else: assert len(duration) == len(initials) - # 4. format the inputs - if len(inputs): - if isinstance(inputs[0], (tuple, list)): - inputs = [(self.traj_group,) + tuple(input) for input in inputs] - elif isinstance(inputs[0], str): - inputs = [(self.traj_group,) + tuple(inputs)] - else: - raise errors.ModelUseError() - # 5. run the network for init_i, initial in enumerate(initials): - # 5.1 set the initial value - for key, val in self.traj_initial.items(): - self.traj_group.ST[key] = val - for key in self.dvar_names: - self.traj_group.ST[key] = initial[key] - for key, val in self.fixed_vars.items(): - if key in self.traj_group.ST: - self.traj_group.ST[key] = val + traj_group = Trajectory(size=1, + integrals=self.model.integrals, + target_vars=initial, + fixed_vars=self.fixed_vars, + pars_update=self.pars_update, + scope=self.model.scopes) # 5.2 run the model - self.traj_net.run(duration=duration[init_i], inputs=inputs, - report=False, data_to_host=True, verbose=False) - x_data = self.traj_group.mon[self.x_var][:, 0] - y_data = self.traj_group.mon[self.y_var][:, 0] + traj_group.run(duration=duration[init_i], report=False, ) + x_data = traj_group.mon[self.x_var][:, 0] + y_data = traj_group.mon[self.y_var][:, 0] max_index = utils.find_indexes_of_limit_cycle_max(x_data, tol=tol) if max_index[0] != -1: x_cycle = x_data[max_index[0]: max_index[1]] diff --git a/brainpy/analysis/trajectory.py b/brainpy/analysis/trajectory.py new file mode 100644 index 00000000..201e3480 --- /dev/null +++ b/brainpy/analysis/trajectory.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +from brainpy import backend +from brainpy.simulation.utils import run_model +from brainpy.tools import DictPlus + +__all__ = [ + 'Trajectory', +] + + +class Trajectory(object): + def __init__(self, size, integrals, target_vars, fixed_vars, + pars_update, scope, show_code=False): + """Trajectory Class. + + Parameters + ---------- + size : int, tuple, list + The network size. + integrals : list of functions, function + The integral functions. + target_vars : dict + The target variables, with the format of "{key: initial_v}". + fixed_vars : dict + The fixed variables, with the format of "{key: fixed_v}". + pars_update : dict + The parameters to update. + scope : + """ + if callable(integrals): + integrals = (integrals,) + elif isinstance(integrals, (list, tuple)) and callable(integrals[0]): + integrals = tuple(integrals) + else: + raise ValueError + self.integrals = integrals + self.target_vars = target_vars + self.fixed_vars = fixed_vars + self.pars_update = pars_update + self.scope = {key: val for key, val in scope.items()} + self.show_code = show_code + + # check network size + if isinstance(size, int): + size = (size,) + elif isinstance(size, (tuple, list)): + assert isinstance(size[0], int) + size = tuple(size) + else: + raise ValueError + + # monitors, variables, parameters + self.mon = DictPlus() + self.vars_and_pars = DictPlus() + for key, val in target_vars.items(): + self.vars_and_pars[key] = backend.ones(size) * val + self.mon[key] = backend.zeros((1,) + size) + for key, val in fixed_vars.items(): + self.vars_and_pars[key] = backend.ones(size) * val + for key, val in pars_update.items(): + self.vars_and_pars[key] = val + self.scope['VP'] = self.vars_and_pars + self.scope['MON'] = self.mon + self.scope['_fixed_vars'] = fixed_vars + + code_lines = ['def run_func(_t, _i, _dt):'] + for integral in integrals: + func_name = integral.__name__ + self.scope[func_name] = integral + # update the step function + assigns = [f'VP["{var}"]' for var in integral.variables] + calls = [f'VP["{var}"]' for var in integral.variables] + calls.append('_t') + calls.extend([f'VP["{var}"]' for var in integral.parameters[1:]]) + code_lines.append(f' {", ".join(assigns)} = {func_name}({", ".join(calls)})') + # reassign the fixed variables + for key, val in fixed_vars.items(): + code_lines.append(f' VP["{key}"][:] = _fixed_vars["{key}"]') + # monitor the target variables + for key in target_vars.keys(): + code_lines.append(f' MON["{key}"][_i] = VP["{key}"]') + # compile + code = '\n'.join(code_lines) + if show_code: + print(code) + print(self.scope) + print() + + # recompile + exec(compile(code, '', 'exec'), self.scope) + self.run_func = self.scope['run_func'] + + def run(self, duration, report=False, report_percent=0.1): + if isinstance(duration, (int, float)): + duration = [0, duration] + elif isinstance(duration, (tuple, list)): + assert len(duration) == 2 + duration = tuple(duration) + else: + raise ValueError + + # get the times + times = backend.arange(duration[0], duration[1], backend.get_dt()) + # reshape the monitor + for key in self.mon.keys(): + self.mon[key] = backend.zeros((len(times),) + backend.shape(self.mon[key])[1:]) + # run the model + run_model(run_func=self.run_func, times=times, report=report, report_percent=report_percent) diff --git a/examples/neurons/FitzHugh_Nagumo.py b/examples/neurons/FitzHugh_Nagumo.py index b48413b0..f8173534 100644 --- a/examples/neurons/FitzHugh_Nagumo.py +++ b/examples/neurons/FitzHugh_Nagumo.py @@ -38,12 +38,12 @@ class FitzHughNagumo(bp.NeuGroup): if __name__ == '__main__': FNs = FitzHughNagumo(100, monitors=['V']) - # simulation - FNs.run(300., inputs=('input', 1.), report=True) - bp.visualize.line_plot(FNs.mon.ts, FNs.mon.V, show=True) - - FNs.run(300., inputs=('input', 0.6), report=True) - bp.visualize.line_plot(FNs.mon.ts, FNs.mon.V, show=True) + # # simulation + # FNs.run(duration=300., inputs=('input', 1.), report=True) + # bp.visualize.line_plot(FNs.mon.ts, FNs.mon.V, show=True) + # + # FNs.run(duration=(300., 600.), inputs=('input', 0.6), report=True) + # bp.visualize.line_plot(FNs.mon.ts, FNs.mon.V, show=True) # phase plane analysis phase = bp.analysis.PhasePlane(FNs.integral, @@ -52,6 +52,8 @@ if __name__ == '__main__': pars_update={'Iext': 1., "a": 0.7, 'b': 0.8, 'tau': 12.5}) phase.plot_nullcline() phase.plot_fixed_point() + # phase.plot_trajectory(initials={'V': -1, 'w': 1}, duration=100.) + phase.plot_limit_cycle_by_sim(initials={'V': -1, 'w': 1}, duration=100.) phase.plot_vector_field(show=True) # bifurcation analysis diff --git a/examples/neurons/LIF_model.py b/examples/neurons/LIF_model.py index fa808ef2..d36b8587 100644 --- a/examples/neurons/LIF_model.py +++ b/examples/neurons/LIF_model.py @@ -5,8 +5,12 @@ import numba as nb import brainpy as bp +bp.backend.set('numpy') + class LIF(bp.NeuGroup): + target_backend = ['numpy', 'numba'] + def __init__(self, size, t_refractory=1., V_rest=0., V_reset=-5., V_th=20., R=1., tau=10., **kwargs): # parameters @@ -46,3 +50,13 @@ class LIF(bp.NeuGroup): self.spike[i] = 0. self.V[i] = V self.input[i] = 0. + + +if __name__ == '__main__': + group = LIF(100, monitors=['V'], show_code=True) + + group.run(duration=200., inputs=('input', 26.), report=True) + bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) + + group.run(duration=(200, 400.), report=True) + bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True) -- 2.34.1 From f66ffc08c560c360dd1f433a5d322924fe08866a Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Tue, 23 Mar 2021 15:52:30 +0800 Subject: [PATCH 11/15] Remove integrals2model.py --- brainpy/analysis/base.py | 5 +- brainpy/analysis/bifurcation.py | 9 ++- brainpy/analysis/integrals2model.py | 89 -------------------------- brainpy/analysis/phase_plane.py | 4 +- brainpy/analysis/utils.py | 80 +++++++++++++++++++++++ develop/benchmark/COBA/COBA_brainpy.py | 2 + 6 files changed, 90 insertions(+), 99 deletions(-) delete mode 100644 brainpy/analysis/integrals2model.py diff --git a/brainpy/analysis/base.py b/brainpy/analysis/base.py index 73e83d5f..3abbaed2 100644 --- a/brainpy/analysis/base.py +++ b/brainpy/analysis/base.py @@ -8,7 +8,6 @@ import sympy from brainpy import errors from brainpy import tools -from brainpy.analysis import integrals2model from brainpy.analysis import solver from brainpy.analysis import utils from brainpy.integrators import sympy_analysis @@ -82,11 +81,11 @@ class BaseNeuronAnalyzer(object): # model # ----- - if isinstance(model_or_integrals, integrals2model.DynamicModel): + if isinstance(model_or_integrals, utils.DynamicModel): self.model = model_or_integrals elif (isinstance(model_or_integrals, (tuple, list)) and callable(model_or_integrals[0])) or \ callable(model_or_integrals): - self.model = integrals2model.transform_integrals_to_model(model_or_integrals) + self.model = utils.transform_integrals_to_model(model_or_integrals) else: raise ValueError diff --git a/brainpy/analysis/bifurcation.py b/brainpy/analysis/bifurcation.py index 892fcf99..64fbb6e2 100644 --- a/brainpy/analysis/bifurcation.py +++ b/brainpy/analysis/bifurcation.py @@ -10,7 +10,6 @@ from mpl_toolkits.mplot3d import Axes3D from brainpy import backend from brainpy import errors from brainpy.analysis import base -from brainpy.analysis import integrals2model from brainpy.analysis import stability from brainpy.analysis import utils from brainpy.analysis.trajectory import Trajectory @@ -48,7 +47,7 @@ class Bifurcation(object): def __init__(self, integrals, target_pars, target_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): # check "model" - self.model = integrals2model.transform_integrals_to_model(integrals) + self.model = utils.transform_integrals_to_model(integrals) # check "target_pars" if not isinstance(target_pars, dict): @@ -454,7 +453,7 @@ class FastSlowBifurcation(object): def __init__(self, integrals, fast_vars, slow_vars, fixed_vars=None, pars_update=None, numerical_resolution=0.1, options=None): # check "model" - self.model = integrals2model.transform_integrals_to_model(integrals) + self.model = utils.transform_integrals_to_model(integrals) # check "fast_vars" if not isinstance(fast_vars, dict): @@ -530,10 +529,10 @@ class FastSlowBifurcation(object): class _FastSlowTrajectory(object): def __init__(self, model_or_intgs, fast_vars, slow_vars, fixed_vars=None, pars_update=None, **kwargs): - if isinstance(model_or_intgs, integrals2model.DynamicModel): + if isinstance(model_or_intgs, utils.DynamicModel): self.model = model_or_intgs elif (isinstance(model_or_intgs, (list, tuple)) and callable(model_or_intgs[0])) or callable(model_or_intgs): - self.model = integrals2model.transform_integrals_to_model(model_or_intgs) + self.model = utils.transform_integrals_to_model(model_or_intgs) else: raise ValueError self.fast_vars = fast_vars diff --git a/brainpy/analysis/integrals2model.py b/brainpy/analysis/integrals2model.py deleted file mode 100644 index d4c05fe8..00000000 --- a/brainpy/analysis/integrals2model.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- - - -import inspect - -from brainpy import errors -from brainpy.integrators import ast_analysis -from brainpy.integrators import sympy_analysis - -try: - from numba.core.dispatcher import Dispatcher -except ModuleNotFoundError: - Dispatcher = None - -__all__ = [ - 'transform_integrals_to_model', - 'DynamicModel', -] - - -def transform_integrals_to_model(integrals): - if callable(integrals): - integrals = [integrals] - - all_scope = dict() - all_variables = set() - all_parameters = set() - analyzers = [] - for integral in integrals: - # integral function - if Dispatcher is not None and isinstance(integral, Dispatcher): - integral = integral.py_func - else: - integral = integral - - # original function - f = integral.origin_f - if Dispatcher is not None and isinstance(f, Dispatcher): - f = f.py_func - func_name = f.__name__ - - # code scope - closure_vars = inspect.getclosurevars(f) - code_scope = dict(closure_vars.nonlocals) - code_scope.update(dict(closure_vars.globals)) - - # separate variables - analysis = ast_analysis.separate_variables(f) - variables_for_returns = analysis['variables_for_returns'] - expressions_for_returns = analysis['expressions_for_returns'] - for vi, (key, vars) in enumerate(variables_for_returns.items()): - variables = [] - for v in vars: - if len(v) > 1: - raise ValueError('Cannot analyze must assignment code line.') - variables.append(v[0]) - expressions = expressions_for_returns[key] - var_name = integral.variables[vi] - DE = sympy_analysis.SingleDiffEq(var_name=var_name, - variables=variables, - expressions=expressions, - derivative_expr=key, - scope=code_scope, - func_name=func_name) - analyzers.append(DE) - - # others - for var in integral.variables: - if var in all_variables: - raise errors.ModelDefError(f'Variable {var} has been defined before. Cannot group ' - f'this integral as a dynamic system.') - all_variables.add(var) - all_parameters.update(integral.parameters) - all_scope.update(code_scope) - - return DynamicModel(integrals=integrals, - analyzers=analyzers, - variables=list(all_variables), - parameters=list(all_parameters), - scopes=all_scope) - - -class DynamicModel(object): - def __init__(self, integrals, analyzers, variables, parameters, scopes): - self.integrals = integrals - self.analyzers = analyzers - self.variables = variables - self.parameters = parameters - self.scopes = scopes diff --git a/brainpy/analysis/phase_plane.py b/brainpy/analysis/phase_plane.py index 1654e6c9..279f0e89 100644 --- a/brainpy/analysis/phase_plane.py +++ b/brainpy/analysis/phase_plane.py @@ -6,7 +6,7 @@ import numpy as np from brainpy import backend from brainpy import errors from brainpy.analysis import base -from brainpy.analysis import integrals2model +from brainpy.analysis import utils from brainpy.analysis import stability from brainpy.analysis import utils from brainpy.analysis.trajectory import Trajectory @@ -86,7 +86,7 @@ class PhasePlane(object): options=None, ): # check "model" - self.model = integrals2model.transform_integrals_to_model(integrals) + self.model = utils.transform_integrals_to_model(integrals) # check "target_vars" if not isinstance(target_vars, dict): diff --git a/brainpy/analysis/utils.py b/brainpy/analysis/utils.py index 9b77bc4a..cdfc698c 100644 --- a/brainpy/analysis/utils.py +++ b/brainpy/analysis/utils.py @@ -1,5 +1,13 @@ # -*- coding: utf-8 -*- + + +import inspect + +from brainpy import errors +from brainpy.integrators import ast_analysis +from brainpy.integrators import sympy_analysis + import _thread as thread import inspect import threading @@ -18,6 +26,8 @@ except ModuleNotFoundError: Dispatcher = None __all__ = [ + 'transform_integrals_to_model', + 'DynamicModel', 'rescale', 'timeout', 'jit_compile', @@ -26,6 +36,76 @@ __all__ = [ ] +def transform_integrals_to_model(integrals): + if callable(integrals): + integrals = [integrals] + + all_scope = dict() + all_variables = set() + all_parameters = set() + analyzers = [] + for integral in integrals: + # integral function + if Dispatcher is not None and isinstance(integral, Dispatcher): + integral = integral.py_func + else: + integral = integral + + # original function + f = integral.origin_f + if Dispatcher is not None and isinstance(f, Dispatcher): + f = f.py_func + func_name = f.__name__ + + # code scope + closure_vars = inspect.getclosurevars(f) + code_scope = dict(closure_vars.nonlocals) + code_scope.update(dict(closure_vars.globals)) + + # separate variables + analysis = ast_analysis.separate_variables(f) + variables_for_returns = analysis['variables_for_returns'] + expressions_for_returns = analysis['expressions_for_returns'] + for vi, (key, vars) in enumerate(variables_for_returns.items()): + variables = [] + for v in vars: + if len(v) > 1: + raise ValueError('Cannot analyze must assignment code line.') + variables.append(v[0]) + expressions = expressions_for_returns[key] + var_name = integral.variables[vi] + DE = sympy_analysis.SingleDiffEq(var_name=var_name, + variables=variables, + expressions=expressions, + derivative_expr=key, + scope=code_scope, + func_name=func_name) + analyzers.append(DE) + + # others + for var in integral.variables: + if var in all_variables: + raise errors.ModelDefError(f'Variable {var} has been defined before. Cannot group ' + f'this integral as a dynamic system.') + all_variables.add(var) + all_parameters.update(integral.parameters) + all_scope.update(code_scope) + + return DynamicModel(integrals=integrals, + analyzers=analyzers, + variables=list(all_variables), + parameters=list(all_parameters), + scopes=all_scope) + + +class DynamicModel(object): + def __init__(self, integrals, analyzers, variables, parameters, scopes): + self.integrals = integrals + self.analyzers = analyzers + self.variables = variables + self.parameters = parameters + self.scopes = scopes + def rescale(min_max, scale=0.01): """Rescale lim.""" diff --git a/develop/benchmark/COBA/COBA_brainpy.py b/develop/benchmark/COBA/COBA_brainpy.py index 16ae6f3e..032a92cc 100644 --- a/develop/benchmark/COBA/COBA_brainpy.py +++ b/develop/benchmark/COBA/COBA_brainpy.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- + + import time import numpy as np -- 2.34.1 From 05479daa3aebe0a62cb1f6922953d4487466a406 Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Tue, 23 Mar 2021 16:51:39 +0800 Subject: [PATCH 12/15] Update new version of API document --- .gitignore | 8 +- brainpy/analysis/__init__.py | 2 + brainpy/analysis/phase_plane.py | 1 - brainpy/analysis/stability.py | 22 + brainpy/analysis/utils.py | 12 +- brainpy/backend/__init__.py | 36 ++ brainpy/integrators/__init__.py | 1 + brainpy/simulation/__init__.py | 6 + brainpy/tools/dicts.py | 2 +- develop/conda-recipe/meta.yaml | 1 - docs/advanced/Limitations.rst | 0 docs/advanced/debugging.ipynb | 393 ------------------ docs/advanced/differential_equations.ipynb | 232 ----------- docs/apis/backend.rst | 18 + docs/apis/core.rst | 43 -- docs/apis/errors.rst | 6 +- docs/apis/inputs.rst | 17 - docs/apis/integration.rst | 98 ----- docs/apis/integrators.rst | 39 ++ docs/apis/profile.rst | 29 -- docs/apis/simulation.rst | 56 +++ docs/apis/tools.rst | 35 +- docs/index.rst | 15 +- .../HH_model_in_ANNarchy.py | 0 .../gapjunction_lif_in_brian2.py | 0 .../how_it_works.ipynb | 46 +- .../numerical_integrators.rst | 0 .../object-oriented_programming.ipynb | 0 .../repeat_mode.ipynb | 8 +- .../tips_on_numba_backend.rst} | 0 .../usage_of_inputs_module.ipynb | 0 .../usage_of_inputs_module.py | 0 .../visualization.ipynb | 0 examples/others/lorenz_system.py | 3 +- 34 files changed, 234 insertions(+), 895 deletions(-) delete mode 100644 docs/advanced/Limitations.rst delete mode 100644 docs/advanced/debugging.ipynb delete mode 100644 docs/advanced/differential_equations.ipynb create mode 100644 docs/apis/backend.rst delete mode 100644 docs/apis/core.rst delete mode 100644 docs/apis/integration.rst create mode 100644 docs/apis/integrators.rst delete mode 100644 docs/apis/profile.rst create mode 100644 docs/apis/simulation.rst rename docs/{advanced => tutorials_advanced}/HH_model_in_ANNarchy.py (100%) rename docs/{advanced => tutorials_advanced}/gapjunction_lif_in_brian2.py (100%) rename docs/{advanced => tutorials_advanced}/how_it_works.ipynb (86%) rename docs/{advanced => tutorials_advanced}/numerical_integrators.rst (100%) rename docs/{advanced => tutorials_advanced}/object-oriented_programming.ipynb (100%) rename docs/{advanced => tutorials_advanced}/repeat_mode.ipynb (99%) rename docs/{advanced/tips_on_jit.rst => tutorials_advanced/tips_on_numba_backend.rst} (100%) rename docs/{advanced => tutorials_advanced}/usage_of_inputs_module.ipynb (100%) rename docs/{advanced => tutorials_advanced}/usage_of_inputs_module.py (100%) rename docs/{advanced => tutorials_advanced}/visualization.ipynb (100%) diff --git a/.gitignore b/.gitignore index 1421a819..0af79d6f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,14 +16,14 @@ docs/images/connection_methods.pptx docs/tutorials/_autosummary docs/tutorials/.ipynb_checkpoints -docs/advanced/_autosummary -docs/advanced/.ipynb_checkpoints +docs/tutorials_advanced/_autosummary +docs/tutorials_advanced/.ipynb_checkpoints develop/fast_synapse_computation.py develop/fast_synapse_computation2.py docs/apis/_autosummary -docs/advanced/usage_of_inputfactory.py -docs/advanced/usage_of_utils_connect.py +docs/tutorials_advanced/usage_of_inputfactory.py +docs/tutorials_advanced/usage_of_utils_connect.py develop/benchmark/COBA/brian2* develop/benchmark/COBA/annarchy* diff --git a/brainpy/analysis/__init__.py b/brainpy/analysis/__init__.py index 378af970..865c9a90 100644 --- a/brainpy/analysis/__init__.py +++ b/brainpy/analysis/__init__.py @@ -4,4 +4,6 @@ from .base import * from .bifurcation import * from .phase_plane import * from .solver import * +from .stability import * +from .trajectory import * from .utils import * diff --git a/brainpy/analysis/phase_plane.py b/brainpy/analysis/phase_plane.py index 279f0e89..b56a3589 100644 --- a/brainpy/analysis/phase_plane.py +++ b/brainpy/analysis/phase_plane.py @@ -6,7 +6,6 @@ import numpy as np from brainpy import backend from brainpy import errors from brainpy.analysis import base -from brainpy.analysis import utils from brainpy.analysis import stability from brainpy.analysis import utils from brainpy.analysis.trajectory import Trajectory diff --git a/brainpy/analysis/stability.py b/brainpy/analysis/stability.py index bf338ed3..fd44d774 100644 --- a/brainpy/analysis/stability.py +++ b/brainpy/analysis/stability.py @@ -2,6 +2,28 @@ import numpy as np +__all__ = [ + 'CENTER_MANIFOLD', + 'SADDLE_NODE', + 'STABLE_POINT_1D', + 'UNSTABLE_POINT_1D', + 'CENTER_2D', + 'STABLE_NODE_2D', + 'STABLE_FOCUS_2D', + 'STABLE_STAR_2D', + 'STABLE_DEGENERATE_2D', + 'UNSTABLE_NODE_2D', + 'UNSTABLE_FOCUS_2D', + 'UNSTABLE_STAR_2D', + 'UNSTABLE_DEGENERATE_2D', + 'UNSTABLE_LINE_2D', + + 'get_1d_stability_types', + 'get_2d_stability_types', + + 'stability_analysis', +] + CENTER_MANIFOLD = 'center manifold' SADDLE_NODE = 'saddle node' STABLE_POINT_1D = 'stable point' diff --git a/brainpy/analysis/utils.py b/brainpy/analysis/utils.py index cdfc698c..77512fc4 100644 --- a/brainpy/analysis/utils.py +++ b/brainpy/analysis/utils.py @@ -1,22 +1,16 @@ # -*- coding: utf-8 -*- - -import inspect - -from brainpy import errors -from brainpy.integrators import ast_analysis -from brainpy.integrators import sympy_analysis - import _thread as thread import inspect import threading import numpy as np -from brainpy.integrators import sympy_analysis -from brainpy import backend +from brainpy import errors from brainpy import tools +from brainpy.integrators import ast_analysis +from brainpy.integrators import sympy_analysis try: import numba diff --git a/brainpy/backend/__init__.py b/brainpy/backend/__init__.py index a1e70706..94ee8c93 100644 --- a/brainpy/backend/__init__.py +++ b/brainpy/backend/__init__.py @@ -23,6 +23,27 @@ SYSTEM_KEYWORDS = ['_dt', '_t', '_i'] def set(backend, module_or_operations=None, node_runner=None, net_runner=None, dt=None): + """Basic backend setting function. + + Using this function, users can set the backend they prefer. For backend + which is unknown, users can provide `module_or_operations` to specify + the operations needed. Also, users can customize the node runner, or the + network runner, by providing the `node_runner` or `net_runner` keywords. + The default numerical precision `dt` can also be set by this function. + + Parameters + ---------- + backend : str + The backend name. + module_or_operations : module, dict, optional + The module or the a dict containing necessary operations. + node_runner : GeneralNodeRunner + An instance of node runner. + net_runner : GeneralNetRunner + An instance of network runner. + dt : float + The numerical precision. + """ if dt is not None: set_dt(dt) @@ -101,6 +122,21 @@ def set(backend, module_or_operations=None, node_runner=None, net_runner=None, d def set_class_keywords(*args): + """Set the keywords for class specification. + + For example: + + >>> class A(object): + >>> def __init__(cls): + >>> pass + >>> def f(self, ): + >>> pass + + In this case, I use "cls" to denote the "self". So, I can set this by + + >>> set_class_keywords('cls', 'self') + + """ global CLASS_KEYWORDS CLASS_KEYWORDS = list(args) diff --git a/brainpy/integrators/__init__.py b/brainpy/integrators/__init__.py index a8b91803..c19144e6 100644 --- a/brainpy/integrators/__init__.py +++ b/brainpy/integrators/__init__.py @@ -4,6 +4,7 @@ from . import dde from . import fde from . import ode from . import sde +from .ast_analysis import * from .constants import * from .delay_vars import * from .integrate_wrapper import * diff --git a/brainpy/simulation/__init__.py b/brainpy/simulation/__init__.py index c7722cd2..3bb06de7 100644 --- a/brainpy/simulation/__init__.py +++ b/brainpy/simulation/__init__.py @@ -1,4 +1,10 @@ # -*- coding: utf-8 -*- from .brain_objects import * +from .constants import * +from .delay import * +from .dynamic_system import * +from .monitors import * +from .runner import * +from .utils import * diff --git a/brainpy/tools/dicts.py b/brainpy/tools/dicts.py index a0475b66..01da1020 100644 --- a/brainpy/tools/dicts.py +++ b/brainpy/tools/dicts.py @@ -9,7 +9,7 @@ __all__ = [ class DictPlus(dict): - """Python dictionaries with advanced dot notation access. + """Python dictionaries with tutorials_advanced dot notation access. For example: diff --git a/develop/conda-recipe/meta.yaml b/develop/conda-recipe/meta.yaml index 05c23052..078f4c78 100644 --- a/develop/conda-recipe/meta.yaml +++ b/develop/conda-recipe/meta.yaml @@ -18,7 +18,6 @@ requirements: - python - numpy>=1.13 - sympy>=1.2 - - scipy>=1.2.0 - numba>=0.50 - matplotlib>=3.0 - setuptools>=40.0.0 diff --git a/docs/advanced/Limitations.rst b/docs/advanced/Limitations.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/advanced/debugging.ipynb b/docs/advanced/debugging.ipynb deleted file mode 100644 index bb1f27ec..00000000 --- a/docs/advanced/debugging.ipynb +++ /dev/null @@ -1,393 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Debugging" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Even if you write clear and readable code, even if you fully understand your codes, env if you are very familiar with your model, weird bugs will inevitably appear and you will need to debug them in some way. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Fortunately, ``BrainPy`` supports debugging with [pdb](https://docs.python.org/3/library/pdb.html)\n", - "module or [breakpoint](https://docs.python.org/3/library/functions.html#breakpoint) (The latest version of \n", - "BrainPy removes the support of debugging in IDEs). That is to say, you do not need to resort to using bunch \n", - "of `print` statements to see what's happening in their code. On the contrary, you can work with \n", - "Python’s interactive source code debugger to see the state of any variable in your model." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For the variables you are interested in, you just need to add the ``pdb.set_trace()`` or ``breakpoint()`` after \n", - "the code line. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this section, let's take the HH neuron model as an example to illustrate how to debug your\n", - "model within BrainPy." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "ExecuteTime": { - "end_time": "2020-12-29T09:15:31.217240Z", - "start_time": "2020-12-29T09:15:28.875338Z" - } - }, - "outputs": [], - "source": [ - "import brainpy as bp\n", - "import numpy as np\n", - "import pdb" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you want to debug your model, we would like to recommond you to open the ``show_code=True``." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "ExecuteTime": { - "end_time": "2020-12-29T09:15:31.233192Z", - "start_time": "2020-12-29T09:15:31.220227Z" - } - }, - "outputs": [], - "source": [ - "bp.profile.set(show_code=True, jit=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, the HH neuron model is defined as:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "ExecuteTime": { - "end_time": "2020-12-29T09:15:31.332935Z", - "start_time": "2020-12-29T09:15:31.243166Z" - } - }, - "outputs": [], - "source": [ - "E_Na = 50.\n", - "E_K = -77.\n", - "E_leak = -54.387\n", - "C = 1.0\n", - "g_Na = 120.\n", - "g_K = 36.\n", - "g_leak = 0.03\n", - "V_th = 20.\n", - "noise = 1.\n", - "\n", - "ST = bp.types.NeuState(\n", - " {'V': -65., 'm': 0.05, 'h': 0.60,\n", - " 'n': 0.32, 'spike': 0., 'input': 0.}\n", - ")\n", - "\n", - "@bp.integrate\n", - "def int_m(m, _t, V):\n", - " alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10))\n", - " beta = 4.0 * np.exp(-(V + 65) / 18)\n", - " return alpha * (1 - m) - beta * m\n", - "\n", - "@bp.integrate\n", - "def int_h(h, _t, V):\n", - " alpha = 0.07 * np.exp(-(V + 65) / 20.)\n", - " beta = 1 / (1 + np.exp(-(V + 35) / 10))\n", - " return alpha * (1 - h) - beta * h\n", - "\n", - "@bp.integrate\n", - "def int_n(n, _t, V):\n", - " alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10))\n", - " beta = 0.125 * np.exp(-(V + 65) / 80)\n", - " return alpha * (1 - n) - beta * n\n", - "\n", - "@bp.integrate\n", - "def int_V(V, _t, m, h, n, I_ext):\n", - " I_Na = (g_Na * np.power(m, 3.0) * h) * (V - E_Na)\n", - " I_K = (g_K * np.power(n, 4.0))* (V - E_K)\n", - " I_leak = g_leak * (V - E_leak)\n", - " dVdt = (- I_Na - I_K - I_leak + I_ext)/C\n", - " return dVdt, noise / C\n", - "\n", - "def update(ST, _t):\n", - " m = np.clip(int_m(ST['m'], _t, ST['V']), 0., 1.)\n", - " h = np.clip(int_h(ST['h'], _t, ST['V']), 0., 1.)\n", - " n = np.clip(int_n(ST['n'], _t, ST['V']), 0., 1.)\n", - " V = int_V(ST['V'], _t, m, h, n, ST['input'])\n", - " \n", - " pdb.set_trace()\n", - " \n", - " spike = np.logical_and(ST['V'] < V_th, V >= V_th)\n", - " ST['spike'] = spike\n", - " ST['V'] = V\n", - " ST['m'] = m\n", - " ST['h'] = h\n", - " ST['n'] = n\n", - " ST['input'] = 0.\n", - "\n", - "HH = bp.NeuType(ST=ST,\n", - " name='HH_neuron',\n", - " steps=update,\n", - " mode='vector')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this example, we add ``pdb.set_trace()`` after the variables $m$, $h$, $n$ and $V$. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then we can create a neuron group, and try to run this neuron model:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "ExecuteTime": { - "end_time": "2020-12-29T09:16:43.061109Z", - "start_time": "2020-12-29T09:15:31.334919Z" - }, - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "def NeuGroup0_input_step(ST, input_inp,):\n", - " # \"input\" step function of NeuGroup0\n", - " ST[5] += input_inp\n", - " \n", - "\n", - "\n", - "def NeuGroup0_monitor_step(ST, _i, mon_ST_spike,):\n", - " # \"monitor\" step function of NeuGroup0\n", - " mon_ST_spike[_i] = ST[4]\n", - " \n", - "\n", - "\n", - "def NeuGroup0_update(ST, _t,):\n", - " # \"update\" step function of NeuGroup0\n", - " _int_m_m = ST[1]\n", - " _int_m__t = _t\n", - " _int_m_V = ST[0]\n", - " _int_m_alpha = 0.1 * (_int_m_V + 40) / (1 - np.exp(-(_int_m_V + 40) / 10))\n", - " _int_m_beta = 4.0 * np.exp(-(_int_m_V + 65) / 18)\n", - " _dfm_dt = _int_m_alpha * (1 - _int_m_m) - _int_m_beta * _int_m_m\n", - " _int_m_m = 0.1*_dfm_dt + _int_m_m\n", - " _int_m_res = _int_m_m\n", - " m = np.clip(_int_m_res, 0.0, 1.0)\n", - " \n", - " _int_h_h = ST[2]\n", - " _int_h__t = _t\n", - " _int_h_V = ST[0]\n", - " _int_h_alpha = 0.07 * np.exp(-(_int_h_V + 65) / 20.0)\n", - " _int_h_beta = 1 / (1 + np.exp(-(_int_h_V + 35) / 10))\n", - " _dfh_dt = _int_h_alpha * (1 - _int_h_h) - _int_h_beta * _int_h_h\n", - " _int_h_h = 0.1*_dfh_dt + _int_h_h\n", - " _int_h_res = _int_h_h\n", - " h = np.clip(_int_h_res, 0.0, 1.0)\n", - " \n", - " _int_n_n = ST[3]\n", - " _int_n__t = _t\n", - " _int_n_V = ST[0]\n", - " _int_n_alpha = 0.01 * (_int_n_V + 55) / (1 - np.exp(-(_int_n_V + 55) / 10))\n", - " _int_n_beta = 0.125 * np.exp(-(_int_n_V + 65) / 80)\n", - " _dfn_dt = _int_n_alpha * (1 - _int_n_n) - _int_n_beta * _int_n_n\n", - " _int_n_n = 0.1*_dfn_dt + _int_n_n\n", - " _int_n_res = _int_n_n\n", - " n = np.clip(_int_n_res, 0.0, 1.0)\n", - " \n", - " _int_V_V = ST[0]\n", - " _int_V__t = _t\n", - " _int_V_m = m\n", - " _int_V_h = h\n", - " _int_V_n = n\n", - " _int_V_I_ext = ST[5]\n", - " _int_V_I_Na = g_Na * np.power(_int_V_m, 3.0) * _int_V_h * (_int_V_V - E_Na)\n", - " _int_V_I_K = g_K * np.power(_int_V_n, 4.0) * (_int_V_V - E_K)\n", - " _int_V_I_leak = g_leak * (_int_V_V - E_leak)\n", - " _int_V_dVdt = (-_int_V_I_Na - _int_V_I_K - _int_V_I_leak + _int_V_I_ext) / C\n", - " _dfV_dt = _int_V_dVdt\n", - " _V_dW = _normal_like(_int_V_V)\n", - " _dgV_dt = noise / C\n", - " _int_V_V = _int_V_V + 0.316227766016838*_V_dW*_dgV_dt + 0.1*_dfV_dt\n", - " _int_V_res = _int_V_V\n", - " V = _int_V_res\n", - " \n", - " pdb.set_trace()\n", - " \n", - " spike = np.logical_and(ST[0] < V_th, V >= V_th)\n", - " ST[4] = spike\n", - " ST[0] = V\n", - " ST[1] = m\n", - " ST[2] = h\n", - " ST[3] = n\n", - " ST[5] = 0.0\n", - " \n", - "\n", - "\n", - "def step_func(_t, _i, _dt):\n", - " NeuGroup0_runner.input_step(NeuGroup0.ST[\"_data\"], NeuGroup0_runner.input_inp,)\n", - " NeuGroup0_runner.update(NeuGroup0.ST[\"_data\"], _t,)\n", - " NeuGroup0_runner.monitor_step(NeuGroup0.ST[\"_data\"], _i, NeuGroup0.mon[\"spike\"],)\n", - "\n", - "> \u001b[1;32mc:\\users\\oujag\\codes\\projects\\brainpy\\docs\\advanced\u001b[0m(52)\u001b[0;36mupdate\u001b[1;34m()\u001b[0m\n", - "\n", - "ipdb> p m\n", - "array([0.05123855])\n", - "ipdb> p n\n", - "array([0.31995744])\n", - "ipdb> p h\n", - "array([0.59995445])\n", - "ipdb> n\n", - "> \u001b[1;32mc:\\users\\oujag\\codes\\projects\\brainpy\\docs\\advanced\u001b[0m(53)\u001b[0;36mupdate\u001b[1;34m()\u001b[0m\n", - "\n", - "ipdb> n\n", - "> \u001b[1;32mc:\\users\\oujag\\codes\\projects\\brainpy\\docs\\advanced\u001b[0m(54)\u001b[0;36mupdate\u001b[1;34m()\u001b[0m\n", - "\n", - "ipdb> n\n", - "> \u001b[1;32mc:\\users\\oujag\\codes\\projects\\brainpy\\docs\\advanced\u001b[0m(55)\u001b[0;36mupdate\u001b[1;34m()\u001b[0m\n", - "\n", - "ipdb> n\n", - "> \u001b[1;32mc:\\users\\oujag\\codes\\projects\\brainpy\\docs\\advanced\u001b[0m(56)\u001b[0;36mupdate\u001b[1;34m()\u001b[0m\n", - "\n", - "ipdb> n\n", - "> \u001b[1;32mc:\\users\\oujag\\codes\\projects\\brainpy\\docs\\advanced\u001b[0m(57)\u001b[0;36mupdate\u001b[1;34m()\u001b[0m\n", - "\n", - "ipdb> n\n", - "> \u001b[1;32mc:\\users\\oujag\\codes\\projects\\brainpy\\docs\\advanced\u001b[0m(58)\u001b[0;36mupdate\u001b[1;34m()\u001b[0m\n", - "\n", - "ipdb> p ST\n", - "array([[-6.41827214e+01],\n", - " [ 5.12385538e-02],\n", - " [ 5.99954448e-01],\n", - " [ 3.19957442e-01],\n", - " [ 0.00000000e+00],\n", - " [ 1.00000000e+01]])\n", - "ipdb> q\n" - ] - }, - { - "ename": "BdbQuit", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mBdbQuit\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[0mgroup\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mbp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mNeuGroup\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mHH\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mgeometry\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmonitors\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'spike'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mgroup\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1000.\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0minputs\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'input'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m10.\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32m~\\codes\\projects\\BrainPy\\brainpy\\core\\base.py\u001b[0m in \u001b[0;36mrun\u001b[1;34m(self, duration, inputs, report, report_percent)\u001b[0m\n\u001b[0;32m 584\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 585\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mrun_idx\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrun_length\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 586\u001b[1;33m \u001b[0mstep_func\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0m_t\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtimes\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mrun_idx\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0m_i\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mrun_idx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0m_dt\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mdt\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 587\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 588\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mprofile\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrun_on_gpu\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m?\u001b[0m in \u001b[0;36mstep_func\u001b[1;34m(_t, _i, _dt)\u001b[0m\n", - "\u001b[1;32m?\u001b[0m in \u001b[0;36mupdate\u001b[1;34m(ST, _t)\u001b[0m\n", - "\u001b[1;32m?\u001b[0m in \u001b[0;36mupdate\u001b[1;34m(ST, _t)\u001b[0m\n", - "\u001b[1;32m~\\Miniconda3\\envs\\py38\\lib\\bdb.py\u001b[0m in \u001b[0;36mtrace_dispatch\u001b[1;34m(self, frame, event, arg)\u001b[0m\n\u001b[0;32m 86\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;31m# None\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 87\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mevent\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'line'\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 88\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdispatch_line\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 89\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mevent\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'call'\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 90\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdispatch_call\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0marg\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Miniconda3\\envs\\py38\\lib\\bdb.py\u001b[0m in \u001b[0;36mdispatch_line\u001b[1;34m(self, frame)\u001b[0m\n\u001b[0;32m 111\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstop_here\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mor\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbreak_here\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 112\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0muser_line\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 113\u001b[1;33m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mquitting\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mBdbQuit\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 114\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtrace_dispatch\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 115\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mBdbQuit\u001b[0m: " - ] - } - ], - "source": [ - "group = bp.NeuGroup(HH, geometry=1, monitors=['spike'])\n", - "group.run(1000., inputs=('input', 10.))" - ] - } - ], - "metadata": { - "hide_input": false, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": false, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/advanced/differential_equations.ipynb b/docs/advanced/differential_equations.ipynb deleted file mode 100644 index 84e4e8f1..00000000 --- a/docs/advanced/differential_equations.ipynb +++ /dev/null @@ -1,232 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Differential Equations" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import brainpy as bp" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In BrainPy, the difinition of differential equations is supportted by a powerfull decorator ``@bp.integrate``. Users should only explicitly write out the right hand of the differential equations, and BrainPy will automatically integerates your defined differential equations. \n", - "\n", - "BrainPy supports the numerical integration of ordinary differential equations (ODEs) and stochastic differential equations (SDEs). " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## ODEs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "For an ordinary differential equation\n", - "\n", - "$$\n", - "\\frac{dx}{dt} = f(x, t)\n", - "$$\n", - "\n", - "the coding in BrainPy has a general form of:\n", - "\n", - "```python\n", - "\n", - "@bp.integrate\n", - "def func(x, t, other_arguments):\n", - " # ... do some computation\n", - " f = ...\n", - " return f\n", - "\n", - "x_t_plus = func(x_t, t, other_arguments)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SDEs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "For the stochastic differential equation:\n", - "\n", - "$$\n", - "\\frac{dx}{dt} = f(x, t) + g(x, t) dW\n", - "$$\n", - "\n", - "the coding in BrainPy can be conducted as:\n", - "\n", - "```python\n", - "\n", - "@bp.integrate\n", - "def func(x, t, other_arguments):\n", - " # ... do some computation\n", - " f = ...\n", - " g = ...\n", - " return f, g\n", - "\n", - "x_t_plus = func(x_t, t, other_arguments)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2020-11-08T13:31:02.326207Z", - "start_time": "2020-11-08T13:31:02.147805Z" - } - }, - "source": [ - "## Return intermediate values" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "BrainPy also supports the user return the intermediate computed results. Let's take the differential equation of $V$ in Hodgkin–Huxley (HH) neuron model as an example. In HH model, the stochastic differential equation $V$ is expressed as:\n", - "\n", - "\\begin{align}\n", - "C_{m}{\\frac {d V}{dt}}&=-{\\bar {g}}_{K}n^{4}(V-V_{K}) - {\\bar {g}}_{Na}m^{3}h(V-V_{Na}) -{\\bar {g}}_{l}(V-V_{l}) + I^{ext} + I^{noise} * dW, \n", - "\\end{align}\n", - "\n", - "where \n", - "\n", - "- the potassium channel current is $I_{K} = {\\bar {g}}_{K}n^{4}(V-V_{K})$, \n", - "- the sodium channel current is $I_{Na} = {\\bar {g}}_{Na}m^{3}h(V-V_{Na})$, and \n", - "- the leaky current is $I_{L} = {\\bar {g}}_{l}(V-V_{l})$.\n", - "\n", - "The user may not only has the interest of the final value $V$, but also take care of the intermediate value $I_{Na}$, $I_K$ and $I_L$. In BrainPy, this kind of requirement can be coded as:\n", - "\n", - "```python\n", - "\n", - "@bp.integrate\n", - "def func(V, t, m, h, n, Iext):\n", - " INa = gNa * m ** 3 * h * (V - ENa)\n", - " IK = gK * n ** 4 * (V - EK)\n", - " IL = gLeak * (V - ELeak)\n", - " f = (- INa - IK - IL + Isyn) / C\n", - " g = noise / C\n", - " return (f, g), INa, IK, IL\n", - "\n", - "V_t_plus, INa, IK, IL = func(V_t, t, m, h, n, Iext)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "Generally, return intermediate values in ODE function can be coded as:\n", - "\n", - "```python\n", - "\n", - "@bp.integrate\n", - "def func(x, t, other_arguments):\n", - " # ... do some computation\n", - " f = ...\n", - " return (f, ), some_values\n", - "```\n", - "\n", - "Return intermediate values in SDE function can be coded as:\n", - "\n", - "```python\n", - "\n", - "@bp.integrate\n", - "def func(x, t, other_arguments):\n", - " # ... do some computation\n", - " f = ...\n", - " g = ...\n", - " return (f, g), some_values\n", - "```" - ] - } - ], - "metadata": { - "hide_input": false, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": false, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/apis/backend.rst b/docs/apis/backend.rst new file mode 100644 index 00000000..347eab41 --- /dev/null +++ b/docs/apis/backend.rst @@ -0,0 +1,18 @@ +brainpy.backend package +======================== + +.. currentmodule:: brainpy.backend +.. automodule:: brainpy.backend + +.. autosummary:: + :toctree: _autosummary + + set + set_dt + get_dt + set_ops + get_backend + set_class_keywords + set_ops_from_module + + diff --git a/docs/apis/core.rst b/docs/apis/core.rst deleted file mode 100644 index 0ce80299..00000000 --- a/docs/apis/core.rst +++ /dev/null @@ -1,43 +0,0 @@ -brainpy.core package -==================== - -.. currentmodule:: brainpy.core -.. automodule:: brainpy.core - -.. autosummary:: - :toctree: _autosummary - - ObjType - NeuType - SynType - Ensemble - NeuGroup - SynConn - Network - ParsUpdate - delayed - -.. autoclass:: ObjType - :members: - -.. autoclass:: NeuType - :members: - -.. autoclass:: SynType - :members: - -.. autoclass:: Ensemble - :members: - -.. autoclass:: NeuGroup - :members: - -.. autoclass:: SynConn - :members: - -.. autoclass:: Network - :members: add, build, run - -.. autoclass:: ParsUpdate - :members: get, keys, items - diff --git a/docs/apis/errors.rst b/docs/apis/errors.rst index 80e97211..6fcf23da 100644 --- a/docs/apis/errors.rst +++ b/docs/apis/errors.rst @@ -9,7 +9,7 @@ brainpy.errors package ModelDefError ModelUseError - TypeMismatchError - IntegratorError - DiffEquationError + DiffEqError CodeError + AnalyzerError + PackageMissingError diff --git a/docs/apis/inputs.rst b/docs/apis/inputs.rst index 69ba993e..95afd2d3 100644 --- a/docs/apis/inputs.rst +++ b/docs/apis/inputs.rst @@ -10,21 +10,4 @@ brainpy.inputs package constant_current spike_current ramp_current - PoissonInput - SpikeTimeInput - FreqInput - - -.. autoclass:: PoissonInput - :toctree: - :members: - -.. autoclass:: SpikeTimeInput - :toctree: - :members: - -.. autoclass:: FreqInput - :toctree: - :members: - diff --git a/docs/apis/integration.rst b/docs/apis/integration.rst deleted file mode 100644 index ce44f8f9..00000000 --- a/docs/apis/integration.rst +++ /dev/null @@ -1,98 +0,0 @@ -brainpy.integration package -=========================== - -.. currentmodule:: brainpy.integration -.. automodule:: brainpy.integration - -.. contents:: - :local: - :depth: 1 - - - -``diff_equation`` module ------------------------- - -.. autosummary:: - :toctree: _autosummary - - Expression - DiffEquation - - -.. autoclass:: Expression - :members: - -.. autoclass:: DiffEquation - :members: - - -``integrator`` module ----------------------- - -.. autosummary:: - :toctree: _autosummary - - integrate - Integrator - Euler - Heun - MidPoint - RK2 - RK3 - RK4 - RK4Alternative - ExponentialEuler - MilsteinIto - MilsteinStra - -.. autoclass:: Integrator - :members: - -.. autoclass:: Euler - :members: - -.. autoclass:: Heun - :members: - -.. autoclass:: MidPoint - :members: - -.. autoclass:: RK2 - :members: - -.. autoclass:: RK3 - :members: - -.. autoclass:: RK4 - :members: - -.. autoclass:: RK4Alternative - :members: - -.. autoclass:: ExponentialEuler - :members: - -.. autoclass:: MilsteinIto - :members: - -.. autoclass:: MilsteinStra - :members: - - -``sympy_tools`` module ----------------------- - -.. autosummary:: - :toctree: _autosummary - - Parser - Printer - str2sympy - sympy2str - -.. autoclass:: Parser - :members: - -.. autoclass:: Printer - :members: diff --git a/docs/apis/integrators.rst b/docs/apis/integrators.rst new file mode 100644 index 00000000..7c3f5645 --- /dev/null +++ b/docs/apis/integrators.rst @@ -0,0 +1,39 @@ +brainpy.integrators package +=========================== + +.. currentmodule:: brainpy.integrators +.. automodule:: brainpy.integrators + +.. contents:: + :local: + :depth: 1 + + + +General functions +----------------- + +.. autosummary:: + :toctree: _autosummary + + odeint + sdeint + ddeint + fdeint + set_default_odeint + get_default_odeint + set_default_sdeint + get_default_sdeint + +``ast_analysis`` module +----------------------- + +.. autosummary:: + :toctree: _autosummary + + DiffEqReader + separate_variables + +.. autoclass:: DiffEqReader + :members: + diff --git a/docs/apis/profile.rst b/docs/apis/profile.rst deleted file mode 100644 index 8e57ea9e..00000000 --- a/docs/apis/profile.rst +++ /dev/null @@ -1,29 +0,0 @@ -brainpy.profile package -======================= - -.. currentmodule:: brainpy.profile -.. automodule:: brainpy.profile - -.. autosummary:: - :toctree: _autosummary - - set - run_on_cpu - run_on_gpu - set_device - get_device - set_dt - get_dt - set_numerical_method - get_numerical_method - set_numba_profile - get_numba_profile - set_backend - get_backend - get_num_thread_gpu - is_jit - is_merge_integrators - is_merge_steps - is_substitute_equation - show_code_scope - show_format_code diff --git a/docs/apis/simulation.rst b/docs/apis/simulation.rst new file mode 100644 index 00000000..91f4a219 --- /dev/null +++ b/docs/apis/simulation.rst @@ -0,0 +1,56 @@ +brainpy.simulation package +========================== + +.. currentmodule:: brainpy.simulation +.. automodule:: brainpy.simulation + + +Basic methods +------------- + + +.. autosummary:: + :toctree: _autosummary + + DynamicSystem + ConstantDelay + Monitor + run_model + + +.. autoclass:: DynamicSystem + :members: build, run, get_schedule, set_schedule + + +.. autoclass:: ConstantDelay + :members: + + +.. autoclass:: Monitor + :members: + + +Brain objects +------------- + +.. autosummary:: + :toctree: _autosummary + + NeuGroup + SynConn + TwoEndConn + Network + +.. autoclass:: NeuGroup + :members: + +.. autoclass:: SynConn + :members: + +.. autoclass:: TwoEndConn + :members: + +.. autoclass:: Network + :members: add, build, run + + diff --git a/docs/apis/tools.rst b/docs/apis/tools.rst index 9dd2ea18..3a156076 100644 --- a/docs/apis/tools.rst +++ b/docs/apis/tools.rst @@ -21,31 +21,14 @@ brainpy.tools package .. autosummary:: :toctree: _autosummary - CodeLineFormatter - format_code - - LineFormatterForTrajectory - format_code_for_trajectory - - FindAtomicOp - find_atomic_op - - FuncCallFinder - replace_func - - DiffEquationAnalyser - analyse_diff_eq - get_identifiers - get_main_code - get_line_indent - indent deindent word_replace is_lambda_function - func_call + get_main_code + get_func_source @@ -60,17 +43,3 @@ brainpy.tools package .. autoclass:: DictPlus :members: - - - -``functions`` module ---------------------- - - -.. autosummary:: - :toctree: _autosummary - - jit - func_copy - numba_func - diff --git a/docs/index.rst b/docs/index.rst index 3da54fa5..28bd2871 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,13 +32,8 @@ Comprehensive examples of BrainPy please see :maxdepth: 1 :caption: Advanced Tutorials - advanced/numerical_integrators - advanced/differential_equations - advanced/repeat_mode - advanced/debugging - advanced/tips_on_jit - advanced/how_it_works - advanced/usage_of_inputs_module + tutorials_advanced/repeat_mode + tutorials_advanced/usage_of_inputs_module .. toctree:: @@ -46,15 +41,15 @@ Comprehensive examples of BrainPy please see :caption: API documentation apis/analysis + apis/backend apis/connectivity - apis/core - apis/integration + apis/integrators + apis/simulation apis/tools apis/visualization apis/errors apis/inputs apis/measure - apis/profile apis/running apis/changelog diff --git a/docs/advanced/HH_model_in_ANNarchy.py b/docs/tutorials_advanced/HH_model_in_ANNarchy.py similarity index 100% rename from docs/advanced/HH_model_in_ANNarchy.py rename to docs/tutorials_advanced/HH_model_in_ANNarchy.py diff --git a/docs/advanced/gapjunction_lif_in_brian2.py b/docs/tutorials_advanced/gapjunction_lif_in_brian2.py similarity index 100% rename from docs/advanced/gapjunction_lif_in_brian2.py rename to docs/tutorials_advanced/gapjunction_lif_in_brian2.py diff --git a/docs/advanced/how_it_works.ipynb b/docs/tutorials_advanced/how_it_works.ipynb similarity index 86% rename from docs/advanced/how_it_works.ipynb rename to docs/tutorials_advanced/how_it_works.ipynb index 87b8c31f..bdc16a07 100644 --- a/docs/advanced/how_it_works.ipynb +++ b/docs/tutorials_advanced/how_it_works.ipynb @@ -61,23 +61,10 @@ " (the same code runs on CPU, multi-core, GPU, OpenCL, etc.).\n", "\n" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## BrainPy is more flexbile than what you think" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Actually, the above illustration is just a tip of the iceberg. BrainPy is much more flebible than what you think. We will come back this section lator." - ] } ], "metadata": { + "hide_input": false, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -93,7 +80,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.9" }, "toc": { "base_numbering": 1, @@ -107,6 +94,35 @@ "toc_position": {}, "toc_section_display": true, "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false } }, "nbformat": 4, diff --git a/docs/advanced/numerical_integrators.rst b/docs/tutorials_advanced/numerical_integrators.rst similarity index 100% rename from docs/advanced/numerical_integrators.rst rename to docs/tutorials_advanced/numerical_integrators.rst diff --git a/docs/advanced/object-oriented_programming.ipynb b/docs/tutorials_advanced/object-oriented_programming.ipynb similarity index 100% rename from docs/advanced/object-oriented_programming.ipynb rename to docs/tutorials_advanced/object-oriented_programming.ipynb diff --git a/docs/advanced/repeat_mode.ipynb b/docs/tutorials_advanced/repeat_mode.ipynb similarity index 99% rename from docs/advanced/repeat_mode.ipynb rename to docs/tutorials_advanced/repeat_mode.ipynb index 9c953b8a..1b7811e8 100644 --- a/docs/advanced/repeat_mode.ipynb +++ b/docs/tutorials_advanced/repeat_mode.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Repeat Mode of ``Network``" + "# Repeat Running" ] }, { @@ -558,9 +558,9 @@ "metadata": { "hide_input": false, "kernelspec": { - "display_name": "base", + "display_name": "Python 3", "language": "python", - "name": "base" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -572,7 +572,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.9" }, "toc": { "base_numbering": 1, diff --git a/docs/advanced/tips_on_jit.rst b/docs/tutorials_advanced/tips_on_numba_backend.rst similarity index 100% rename from docs/advanced/tips_on_jit.rst rename to docs/tutorials_advanced/tips_on_numba_backend.rst diff --git a/docs/advanced/usage_of_inputs_module.ipynb b/docs/tutorials_advanced/usage_of_inputs_module.ipynb similarity index 100% rename from docs/advanced/usage_of_inputs_module.ipynb rename to docs/tutorials_advanced/usage_of_inputs_module.ipynb diff --git a/docs/advanced/usage_of_inputs_module.py b/docs/tutorials_advanced/usage_of_inputs_module.py similarity index 100% rename from docs/advanced/usage_of_inputs_module.py rename to docs/tutorials_advanced/usage_of_inputs_module.py diff --git a/docs/advanced/visualization.ipynb b/docs/tutorials_advanced/visualization.ipynb similarity index 100% rename from docs/advanced/visualization.ipynb rename to docs/tutorials_advanced/visualization.ipynb diff --git a/examples/others/lorenz_system.py b/examples/others/lorenz_system.py index bf04957d..1e38d9d9 100644 --- a/examples/others/lorenz_system.py +++ b/examples/others/lorenz_system.py @@ -24,8 +24,7 @@ class LorenzSystem(bp.DynamicSystem): def lorenz_g(x, y, z, t, sigma, rho, beta, p): return p * x, p * y, p * z - @bp.sdeint(g=lorenz_g, sde_type=bp.ITO_SDE, - wiener_type=bp.SCALAR_WIENER) + @bp.sdeint(g=lorenz_g) def lorenz_f(x, y, z, t, sigma, rho, beta, p): dx = sigma * (y - x) dy = x * (rho - z) - y -- 2.34.1 From 497d14fec1858e9f5ff8601065c35c014990f131 Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Tue, 23 Mar 2021 23:53:48 +0800 Subject: [PATCH 13/15] Update docs --- README.md | 42 +- brainpy/backend/operators/bk_jax.py | 3 +- brainpy/backend/operators/bk_tensorflow.py | 6 +- brainpy/integrators/integrate_wrapper.py | 20 +- develop/benchmark/COBA/COBA_brainpy.py | 175 +++-- docs/apis/analysis.rst | 4 +- docs/apis/backend.rst | 4 +- docs/apis/connectivity.rst | 4 +- docs/apis/errors.rst | 4 +- docs/apis/inputs.rst | 4 +- docs/apis/integrators.rst | 4 +- docs/apis/measure.rst | 4 +- docs/apis/running.rst | 4 +- docs/apis/simulation.rst | 4 +- docs/apis/tools.rst | 4 +- docs/apis/visualization.rst | 4 +- docs/images/speed_scaling.png | Bin 32972 -> 0 bytes docs/index.rst | 12 +- ...sis.ipynb => neurodynamics_analysis.ipynb} | 36 +- docs/tutorials/neurodynamics_simulation.ipynb | 108 ++++ docs/tutorials/numerical_solvers.ipynb | 605 ++++++++++++++++++ 21 files changed, 896 insertions(+), 155 deletions(-) delete mode 100644 docs/images/speed_scaling.png rename docs/tutorials/{neuron_analysis.ipynb => neurodynamics_analysis.ipynb} (99%) create mode 100644 docs/tutorials/neurodynamics_simulation.ipynb create mode 100644 docs/tutorials/numerical_solvers.ipynb diff --git a/README.md b/README.md index 13f912fd..9a38c728 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![Logo](docs/images/logo.png) -[![LICENSE](https://anaconda.org/brainpy/brainpy/badges/license.svg)](https://github.com/PKU-NIP-Lab/BrainPy) [![Documentation](https://readthedocs.org/projects/brainpy/badge/?version=latest)](https://brainpy.readthedocs.io/en/latest/?badge=latest) [![Conda](https://anaconda.org/brainpy/brainpy-simulator/badges/version.svg)](https://anaconda.org/brainpy/brainpy-simulator) [![PyPI version](https://badge.fury.io/py/brainpy-simulator.svg)](https://badge.fury.io/py/brainpy-simulator) [![travis](https://travis-ci.org/PKU-NIP-Lab/BrainPy.svg?branch=master)](https://travis-ci.org/PKU-NIP-Lab/BrainPy) +[![LICENSE](https://anaconda.org/brainpy/brainpy/badges/license.svg)](https://github.com/PKU-NIP-Lab/BrainPy) [![Documentation](https://readthedocs.org/projects/brainpy/badge/?version=latest)](https://brainpy.readthedocs.io/en/latest/?badge=latest) [![Conda](https://anaconda.org/brainpy/brainpy-simulator/badges/version.svg)](https://anaconda.org/brainpy/brainpy-simulator) [![PyPI version](https://badge.fury.io/py/brainpy-simulator.svg)](https://badge.fury.io/py/brainpy-simulator) @@ -11,12 +11,18 @@ ## Why to use BrainPy -``BrainPy`` is a lightweight framework based on the latest Just-In-Time (JIT) compilers (especially [Numba](https://numba.pydata.org/)). The goal of ``BrainPy`` is to provide a unified simulation and analysis framework for neuronal dynamics with the feature of high flexibility and efficiency. BrainPy is flexible because it endows the users with the fully data/logic flow control. BrainPy is efficient because it supports JIT acceleration on CPUs and GPUs. +``BrainPy`` is an integrative framework for computational neuroscience and brain-inspired computation. Three core functions are provided in `BrainPy`: +- *General numerical solvers* for ODEs and SDEs (future will support DDEs and FDEs). +- *Neurodynamics simulation tools* for brain objects, such like neurons, synapses and networks (future wille support soma and dendrites). +- *Neurodynamics analysis tools* for differential equations, including phase plane analysis and bifurcation analysis (future will support continuation analysis and sensitive analysis). -![Speed Comparison with Brian2](docs/images/speed.png) +Moreover, `BrainPy` can effectively satisfy your basic requirements: 1. *Easy to learn and use*, because it is only based on Python language and has little dependency requirements; 2. *Highly flexible and transparent*, because it endows the users with the fully data/logic flow control; 3. *Simulation can be guided with the analysis*, because the same code in BrainPy can not only be used for simulation, but also for dynamics analysis; 4. *Efficient running speed*, because BrainPy is compatitable with the latest JIT compilers (or any other computing backend you prefer). -![Scaling of BrainPy](docs/images/speed_scaling.png) + + + +![Speed Comparison](docs/images/speed.png) @@ -24,30 +30,34 @@ Install ``BrainPy`` using ``pip``: - > pip install brainpy-simulator +```bash +> pip install brainpy-simulator +``` Install ``BrainPy`` using ``conda``: - > conda install brainpy-simulator -c brainpy +```bash +> conda install brainpy-simulator -c brainpy +``` Install ``BrainPy`` from source: - > pip install git+https://github.com/PKU-NIP-Lab/BrainPy - > # or - > pip install git+https://git.openi.org.cn/OpenI/BrainPy - > # or - > pip install -e git://github.com/PKU-NIP-Lab/BrainPy.git@V0.2.5 +```bash +> pip install git+https://github.com/PKU-NIP-Lab/BrainPy +> # or +> pip install git+https://git.openi.org.cn/OpenI/BrainPy +> # or +> pip install -e git://github.com/PKU-NIP-Lab/BrainPy.git@V0.2.5 +``` -``BrainPy`` is based on Python (>=3.7), and the following packages are -required to be installed to use ``BrainPy``: +``BrainPy`` is based on Python (>=3.7), and the following packages are required to be installed to use ``BrainPy``: - NumPy >= 1.13 -- SymPy >= 1.2 -- SciPy >= 1.2 -- Numba >= 0.50.0 - Matplotlib >= 3.0 + + ## Neurodynamics simulation diff --git a/brainpy/backend/operators/bk_jax.py b/brainpy/backend/operators/bk_jax.py index e77ae3d4..6904c3f7 100644 --- a/brainpy/backend/operators/bk_jax.py +++ b/brainpy/backend/operators/bk_jax.py @@ -20,8 +20,7 @@ exp = numpy.exp sum = numpy.sum zeros = numpy.zeros eye = numpy.eye -outer = numpy.outer -dot = numpy.dot +matmul = numpy.matmul vstack = numpy.vstack arange = numpy.arange diff --git a/brainpy/backend/operators/bk_tensorflow.py b/brainpy/backend/operators/bk_tensorflow.py index 19881ba4..f979f0a5 100644 --- a/brainpy/backend/operators/bk_tensorflow.py +++ b/brainpy/backend/operators/bk_tensorflow.py @@ -12,14 +12,10 @@ exp = tf.math.exp sum = tf.math.reduce_sum zeros = tf.zeros eye = tf.eye -dot = tf.matmul +matmul = tf.matmul arange = tf.range -def outer(A, B): - return tf.tensordot(A, B, axes=0) - - def vstack(values): return tf.concat(values, axis=1) diff --git a/brainpy/integrators/integrate_wrapper.py b/brainpy/integrators/integrate_wrapper.py index 3083ddca..63faf225 100644 --- a/brainpy/integrators/integrate_wrapper.py +++ b/brainpy/integrators/integrate_wrapper.py @@ -4,6 +4,10 @@ from . import ode from . import sde __all__ = [ + 'SUPPORTED_ODE_METHODS', + 'SUPPORTED_SDE_METHODS', + + 'odeint', 'sdeint', 'ddeint', @@ -17,8 +21,8 @@ __all__ = [ _DEFAULT_ODE_METHOD = 'euler' _DEFAULT_SDE_METHOD = 'euler' -SUPPORTED_ODE = [m for m in dir(ode) if not m.startswith('__')] -SUPPORTED_SDE = [m for m in dir(sde) if not m.startswith('__')] +SUPPORTED_ODE_METHODS = [m for m in dir(ode) if not m.startswith('__') and callable(getattr(ode, m))] +SUPPORTED_SDE_METHODS = [m for m in dir(sde) if not m.startswith('__') and callable(getattr(sde, m))] def _wrapper(f, method, module, **kwargs): @@ -29,9 +33,9 @@ def _wrapper(f, method, module, **kwargs): def odeint(f=None, method=None, **kwargs): if method is None: method = _DEFAULT_ODE_METHOD - if method not in SUPPORTED_ODE: + if method not in SUPPORTED_ODE_METHODS: raise ValueError(f'Unknown ODE numerical method "{method}". Currently ' - f'BrainPy only support: {SUPPORTED_ODE}') + f'BrainPy only support: {SUPPORTED_ODE_METHODS}') if f is None: return lambda f: _wrapper(f, method=method, module=ode, **kwargs) @@ -42,9 +46,9 @@ def odeint(f=None, method=None, **kwargs): def sdeint(f=None, method=None, **kwargs): if method is None: method = _DEFAULT_SDE_METHOD - if method not in SUPPORTED_SDE: + if method not in SUPPORTED_SDE_METHODS: raise ValueError(f'Unknown SDE numerical method "{method}". Currently ' - f'BrainPy only support: {SUPPORTED_SDE}') + f'BrainPy only support: {SUPPORTED_SDE_METHODS}') if f is None: return lambda f: _wrapper(f, method=method, module=sde, **kwargs) @@ -70,7 +74,7 @@ def set_default_odeint(method): """ if not isinstance(method, str): raise ValueError(f'Only support string, not {type(method)}.') - if method not in SUPPORTED_ODE: + if method not in SUPPORTED_ODE_METHODS: raise ValueError(f'Unsupported ODE numerical method: {method}.') global _DEFAULT_ODE_METHOD @@ -98,7 +102,7 @@ def set_default_sdeint(method): """ if not isinstance(method, str): raise ValueError(f'Only support string, not {type(method)}.') - if method not in SUPPORTED_SDE: + if method not in SUPPORTED_SDE_METHODS: raise ValueError(f'Unsupported SDE numerical method: {method}.') global _DEFAULT_SDE_METHOD diff --git a/develop/benchmark/COBA/COBA_brainpy.py b/develop/benchmark/COBA/COBA_brainpy.py index 032a92cc..7c06b339 100644 --- a/develop/benchmark/COBA/COBA_brainpy.py +++ b/develop/benchmark/COBA/COBA_brainpy.py @@ -8,7 +8,7 @@ import numpy as np import brainpy as bp dt = 0.05 -bp.profile.set(jit=True, dt=dt) +bp.backend.set('numba', dt=dt) # Parameters num_exc = 3200 @@ -26,99 +26,92 @@ we = 0.6 # excitatory synaptic weight (voltage) wi = 6.7 # inhibitory synaptic weight ref = 5.0 -neu_ST = bp.types.NeuState( - {'sp_t': -1e7, - 'V': 0., - 'spike': 0., - 'ge': 0., - 'gi': 0.} -) - -@bp.integrate -def int_ge(ge, t): - return - ge / taue - - -@bp.integrate -def int_gi(gi, t): - return - gi / taui - - -@bp.integrate -def int_V(V, t, ge, gi): - return (ge * (Erev_exc - V) + gi * (Erev_inh - V) + (El - V) + I) / taum - - -def neu_update(ST, _t): - ST['ge'] = int_ge(ST['ge'], _t) - ST['gi'] = int_gi(ST['gi'], _t) - - ST['spike'] = 0. - if (_t - ST['sp_t']) > ref: - V = int_V(ST['V'], _t, ST['ge'], ST['gi']) - ST['spike'] = 0. - if V >= Vt: - ST['V'] = Vr - ST['spike'] = 1. - ST['sp_t'] = _t - else: - ST['V'] = V - - -neuron = bp.NeuType(name='COBA', - ST=neu_ST, - steps=neu_update, - mode='scalar') - - -def update1(pre, post, pre2post): - for pre_id in range(len(pre2post)): - if pre['spike'][pre_id] > 0.: - post_ids = pre2post[pre_id] - for i in post_ids: - post['ge'][i] += we - - -exc_syn = bp.SynType('exc_syn', - steps=update1, - ST=bp.types.SynState([]), - mode='vector') - - -def update2(pre, post, pre2post): - for pre_id in range(len(pre2post)): - if pre['spike'][pre_id] > 0.: - post_ids = pre2post[pre_id] - for i in post_ids: - post['gi'][i] += wi - - -inh_syn = bp.SynType('inh_syn', - steps=update2, - ST=bp.types.SynState([]), - mode='vector') - - -group = bp.NeuGroup(neuron, - size=num_exc + num_inh, - monitors=['spike']) -group.ST['V'] = np.random.randn(num_exc + num_inh) * 5. - 55. - -exc_conn = bp.TwoEndConn(exc_syn, - pre=group[:num_exc], - post=group, - conn=bp.connect.FixedProb(prob=0.02)) - -inh_conn = bp.TwoEndConn(inh_syn, - pre=group[num_exc:], - post=group, - conn=bp.connect.FixedProb(prob=0.02)) - -net = bp.Network(group, exc_conn, inh_conn) +class LIF(bp.NeuGroup): + target_backend = ['numpy', 'numba'] + + def __init__(self, size, **kwargs): + # variables + self.V = bp.backend.zeros(size) + self.spike = bp.backend.zeros(size) + self.ge = bp.backend.zeros(size) + self.gi = bp.backend.zeros(size) + self.input = bp.backend.zeros(size) + self.t_last_spike = bp.backend.ones(size) * -1e7 + + super(LIF, self).__init__(size=size, **kwargs) + + @staticmethod + @bp.odeint(method='euler') + def int_g(ge, gi, t): + dge = - ge / taue + dgi = - gi / taui + return dge, dgi + + @staticmethod + @bp.odeint(method='euler') + def int_V(V, t, ge, gi): + dV = (ge * (Erev_exc - V) + gi * (Erev_inh - V) + El - V + I) / taum + return dV + + def update(self, _t): + self.ge, self.gi = self.int_g(self.ge, self.gi, _t) + for i in range(self.size[0]): + self.spike[i] = 0. + if (_t - self.t_last_spike[i]) > ref: + V = self.int_V(self.V[i], _t, self.ge[i], self.gi[i]) + if V >= Vt: + self.V[i] = Vr + self.spike[i] = 1. + self.t_last_spike[i] = _t + else: + self.V[i] = V + self.input[i] = I + + +class EecSyn(bp.TwoEndConn): + target_backend = ['numpy', 'numba'] + + def __init__(self, pre, post, conn, **kwargs): + self.conn = conn(pre.size, post.size) + self.pre2post = self.conn.requires('pre2post') + super(EecSyn, self).__init__(pre=pre, post=post, **kwargs) + + def update(self, _t): + for pre_id, spike in enumerate(self.pre.spike): + if spike > 0: + for post_i in self.pre2post[pre_id]: + self.post.ge[post_i] += we + + +class InhSyn(bp.TwoEndConn): + target_backend = ['numpy', 'numba'] + + def __init__(self, pre, post, conn, **kwargs): + self.conn = conn(pre.size, post.size) + self.pre2post = self.conn.requires('pre2post') + super(InhSyn, self).__init__(pre=pre, post=post, **kwargs) + + def update(self, _t): + for pre_id, spike in enumerate(self.pre.spike): + if spike > 0: + for post_i in self.pre2post[pre_id]: + self.post.gi[post_i] += wi + + +E_group = LIF(num_exc, monitors=['spike']) +E_group.V = np.random.randn(num_exc) * 5. - 55. +I_group = LIF(num_inh, monitors=['spike']) +I_group.V = np.random.randn(num_inh) * 5. - 55. +E2E = EecSyn(pre=E_group, post=E_group, conn=bp.connect.FixedProb(0.02)) +E2I = EecSyn(pre=E_group, post=I_group, conn=bp.connect.FixedProb(0.02)) +I2E = InhSyn(pre=I_group, post=E_group, conn=bp.connect.FixedProb(0.02)) +I2I = InhSyn(pre=I_group, post=I_group, conn=bp.connect.FixedProb(0.02)) + +net = bp.Network(E_group, I_group, E2E, E2I, I2E, I2I) t0 = time.time() net.run(5000., report=True) print('Used time {} s.'.format(time.time() - t0)) -bp.visualize.raster_plot(net.ts, group.mon.spike, show=True) +bp.visualize.raster_plot(net.ts, E_group.mon.spike, show=True) diff --git a/docs/apis/analysis.rst b/docs/apis/analysis.rst index 9d4aaf7d..58cf6f36 100644 --- a/docs/apis/analysis.rst +++ b/docs/apis/analysis.rst @@ -1,5 +1,5 @@ -brainpy.analysis package -======================== +brainpy.analysis +================ .. currentmodule:: brainpy.analysis .. automodule:: brainpy.analysis diff --git a/docs/apis/backend.rst b/docs/apis/backend.rst index 347eab41..a111ea35 100644 --- a/docs/apis/backend.rst +++ b/docs/apis/backend.rst @@ -1,5 +1,5 @@ -brainpy.backend package -======================== +brainpy.backend +=============== .. currentmodule:: brainpy.backend .. automodule:: brainpy.backend diff --git a/docs/apis/connectivity.rst b/docs/apis/connectivity.rst index dc66d8f9..111d4dbc 100644 --- a/docs/apis/connectivity.rst +++ b/docs/apis/connectivity.rst @@ -1,5 +1,5 @@ -brainpy.connect package -============================ +brainpy.connect +=============== .. currentmodule:: brainpy.connectivity .. automodule:: brainpy.connectivity diff --git a/docs/apis/errors.rst b/docs/apis/errors.rst index 6fcf23da..9bb74a5f 100644 --- a/docs/apis/errors.rst +++ b/docs/apis/errors.rst @@ -1,5 +1,5 @@ -brainpy.errors package -============================ +brainpy.errors +============== .. currentmodule:: brainpy.errors .. automodule:: brainpy.errors diff --git a/docs/apis/inputs.rst b/docs/apis/inputs.rst index 95afd2d3..92458162 100644 --- a/docs/apis/inputs.rst +++ b/docs/apis/inputs.rst @@ -1,5 +1,5 @@ -brainpy.inputs package -============================ +brainpy.inputs +============== .. currentmodule:: brainpy.inputs .. automodule:: brainpy.inputs diff --git a/docs/apis/integrators.rst b/docs/apis/integrators.rst index 7c3f5645..a13fcb5b 100644 --- a/docs/apis/integrators.rst +++ b/docs/apis/integrators.rst @@ -1,5 +1,5 @@ -brainpy.integrators package -=========================== +brainpy.integrators +=================== .. currentmodule:: brainpy.integrators .. automodule:: brainpy.integrators diff --git a/docs/apis/measure.rst b/docs/apis/measure.rst index 9013ffe1..e4889607 100644 --- a/docs/apis/measure.rst +++ b/docs/apis/measure.rst @@ -1,5 +1,5 @@ -brainpy.measure package -============================ +brainpy.measure +=============== .. currentmodule:: brainpy.measure .. automodule:: brainpy.measure diff --git a/docs/apis/running.rst b/docs/apis/running.rst index e276e5e1..dcd191f3 100644 --- a/docs/apis/running.rst +++ b/docs/apis/running.rst @@ -1,5 +1,5 @@ -brainpy.running package -============================ +brainpy.running +=============== .. currentmodule:: brainpy.running .. automodule:: brainpy.running diff --git a/docs/apis/simulation.rst b/docs/apis/simulation.rst index 91f4a219..8224f688 100644 --- a/docs/apis/simulation.rst +++ b/docs/apis/simulation.rst @@ -1,5 +1,5 @@ -brainpy.simulation package -========================== +brainpy.simulation +================== .. currentmodule:: brainpy.simulation .. automodule:: brainpy.simulation diff --git a/docs/apis/tools.rst b/docs/apis/tools.rst index 3a156076..caf7fa45 100644 --- a/docs/apis/tools.rst +++ b/docs/apis/tools.rst @@ -1,5 +1,5 @@ -brainpy.tools package -===================== +brainpy.tools +============= .. currentmodule:: brainpy.tools .. automodule:: brainpy.tools diff --git a/docs/apis/visualization.rst b/docs/apis/visualization.rst index fb120f30..7f19ec58 100644 --- a/docs/apis/visualization.rst +++ b/docs/apis/visualization.rst @@ -1,5 +1,5 @@ -brainpy.visualize package -========================= +brainpy.visualize +================= .. currentmodule:: brainpy.visualization .. automodule:: brainpy.visualization diff --git a/docs/images/speed_scaling.png b/docs/images/speed_scaling.png deleted file mode 100644 index b523c88fe3b7cc41201fb3b5dba86de3cc5b8e1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32972 zcmeFZcU05Q*Dgx$2-15mk&g5tgrW$7M5&4tL5dJSKq=Beln&C77AZ=R4hcx_O7AT| zklsQkln}VV?|Fak@80vSd;huXtaHyjvlfeFSj=bk%$_}a@8{W{2+(6KN-`EQ92^`< z?T4CAad7YfI5@b0Bt+Pr+(l$6_7|?lQ!RCzvO%^@>=y!iH9a*PoXR-z%a?@M@1$=Y zKJ&oAp?Q1rgS%kI>5YRE^io?>&CuHd*<=~b)|0h9Lq)jX7Q{iZ{}s3C_At%|5q#O` zFe1MVHGJicZe|ZDhgQXi7r|7*;j@XUf^ur2(k>o^5e)Z6v+H;Yk~Pz0YgdY#XH;gY z?sLd|;C{N3EFu4EisyASCqbn)+e$~eHEl&xQ&?Sie|Yh)AK_>BA7~oe8V0HYabCT% zHMYCF>fB>ZXIALIPWGljc!7|=A8>FK@d<7O$2N!*6Jc-5WnT;A>wmV|SzRFH-#Ya2 z)8CNM{qqkv4@v*?fB(w?|6k~2Ikfj&J`t}>PfxEx285ROwoD&PuU*iE{5LQuwfw)`>rDREnYA=q4y#MPToXL_hv}UemQc#pQIHG@_`m2T{Nq>g; z6SUVER*CG(*&mXfzSw%SbKHgmdsZMCW>BGh;PY9_-ht>}$k_hM`ZmwT0i?aJrGG|N z%S0+-OcK+KyRPsBGB7|L>_A8@B`vbRwBaMyogHWEztEYKSU~=w;Fms!Un5h zPUtP9Vtqo{AW9P!X)0z0BdIMf%mk2K6E3c3ym?^ zEg{W$xsCJ*^`G8TYLSvUWizwzA_-0*&3qdmK4u`T^cp$8J%we0LM3KuY%Qm zJAri&?~!X>m+|}HLleaLDOkU>j+5@oJ2xJ?dBmtD3FGrU&av|6Oau=K!mk*7x0JkY zo!UEA$5rRau^OgfJa*2MpL{qnt_>{*rrTEQx3p$^&YtzXNE5B@7~I>(H2M{ciX+MC zkjymp%k7K9k!GgPT@P{zbj&ly%K-;1EwFO;clP7!Cn{%7vR;dOq?H{=w>2tvGn`gC zJUl|2;rLBDzcr9hkGQ<^@djcd^rrgAdPoK1M3uTR4#$@+UMP3^X)^k18Bg(Hgd_5#=4JewD?9(*C8ON^=I z_l~mL>X`L%yniL{a^NoM-J?B1;kH=2=V)m}s520k1ue2bR$3yL2HrHV*-T9nW5-!* z#Ngma=>0VZPlYdj5T?iW$PTK{ssED2ckFp3{6UDY!2)L+uS0UotX>EbmBv62iA*W( z?f5-z6yq5D?2e&uPi~Y9dk>24#H=y$Q;VhTBP}sSjx-l8P_%3rJ%T~CI6OXs~?kR!2nqnPgWJ?@JPN|fXK zCpi`n)1j|baqn+i_B@H+LRUt7okhhEkcmGwr}~{?LVUysfL^&p#Nl4lPc=(%1oId( z_(ousRg2Aw14H*`JRFFxf8o0FrcEE?T|%+4iGR2_j@w90cwe^$A|8emVzPdO!?zFX-|t!dx1GBe$(rCoYua z8lx;)uC#?fU>KP(cvurM&L}oL!0whS#ym$umRfgtjnFUtO);~Q(==n8{AZgiCfl)UI6h&-6d{PN3z z4|IGaW%-&Ft#LRBM-wzt{no%5|DC_1E^*5~*q;{A1b+ZawN}@`$mkvyq?L=-o4pA& z_i+-3!%i$K>v>g6o4z(p3RCiCZ<1f(?ri!Qjud%c=kA5&WNKl85+oM9nJnr+8zG8ddy=xNY;7;H_? zsA*|rY=2~KSJ&*wg1wtsss3JCj$tQu3;O0V?8YnZB9qv!_1i z*8(P>ViUd%yYSGT@c{?0FJ}fIeFR~`O$xA%mm74Hfe@*T*Sdj9>n=? zC*rpegf1MOc&mDh7xpRw-W>2NyAfo`VzN%uaJd#dtFhQH)BcC`H# zo(;5&SM^~cX{DdVu{i9^$S z7PH-UcAWNFt`|9hD&UHfxNMsn$6>m^9}n)ks(*MS#^WscXL-lc%&t#sZF;VsLX}}; z*A7nA+#OrCkc4p1c`W;IfC#;_qnyEhTu`9rd}*tG&8DP+uxaY@It!=P9&Qd8TJYJq zRqg5^`uo7G_h7pZyxY&YBFMh7_5>OzJFftH5Z)96KRXA`Je}E&ELOoNC9z!02fm!)7cB=%IDXPX*k}h z&4IcUw=C_+g#$%YkNA~X^B+)dv5OPQDXHbQs}DC9hH!~e14v0Ly((JFZ8(+R+2<-h ze>{kK-!R=SUS6fY`klwVJoANB!?j|7 zP}*88a7SqrA``DW2iY?&lSx^$5?DS_qW7xhIO;;3@73J>;%!tleei=nef;+XQBg2( zlgxIIBlMb(K8G5>Tf+l-73(aWp-U|y`^A7^yU|;AZNsk4A4va+GU}e>$DOsVlCK*g zahMW2wU?~E39;IAIxpr2pe<)+TGuJ%8=20hQTD(0Eo^u*Xv zm(h=fvJUX3T;GoEdVLDPOcpKF{LZ4U?}8Plz?%qCvLyNSkHF2lIh7?^&23Rj^w8&N zo`)s^mudOHC%C}=6Hzs_tB^hCbp;Kz?7>jKLT3m6$Vk}6xu@-e>YM{Gk?ZiIu()vIu zz@=yHI#|>^$t2ltQ0CWe$S_hqOy7@N+a)6t7}01cDjYm2KcdT8PuCN>44(g}g71}H z*G{zb0vwOMQ(PcB5=7UbK<&`V_^A7+YN;WaS0Z?}@(nrpOs5C1Z_45i};q+%`F`TqC03Cn!Hxxw5 z+!VXahNnYAxAq>@D3H8<0S6Wd4z^Tm?;%;0Ax4zVtUHGp#4Gqc7j8{=5BH5WJGYc- zWhJtuzsWpFzX*mcy{v?ZQpsC1P%3 z7**T8DVJEb?>UsTT!}GCBL>Y&xzF%qbGwYMCw)A>9S*+|B|1YORKCKWUSdA1{^^GA zTy*h=>J3s;?Vk@DLJHCC&M@P`V8fh(A8Sb?#5z7l2g{#TX!VXmk!_DLUaqW%AH3ry z4uD0i`x#59V8MWZnKwVUeeR#nzq#)Cw(9!*8ofS;S8iIo{6u1sl*7r;qVhcdkDoKu zH?re8Hf2^WXcQgLucaHWK80BKN-AyD=wSxM;P=rjj19{X84r_&r4$CYZNsX} zz0ZBx*Q0KXXBHuK3xXE@bW#GZ0oUP3%Nz_-%NW=XEC!N?dKV7^3w<)3;^(`xg5h_1 zk(i6ot`7uc1wVb-+}E7C!1q_%L=F@fZ^ii^+=5~dm-nuH$??L7XAeI;0tZ~FvJ!dv z%tTQ!oSHXE&b2%oRZd5}_nlsddq7(VPdCRc+fDt!=)@DuDay%^A%poQ#CNOGKs=m& z^d=-|mU@9{^UHzELGK3DeHr4~puMbeUFWcWlsZvaOuR;8uD%=iYw(`HMKl$v_7hDP zq&d%v6Fq5hfU~gSmO!d4!0Exs{Cs}F;&;h2no^qTQq8W(G4YD)iW|SlCY7jbW3jDi zMr(R>>pp7qQnnAV9Z+T`P(meEyw`*1n>B>Ic_nFm;e2#&V@uAv`C<@O-!f=2Xx{bW zU~wHb#3VrE&FObs=qFKQ{Pt+>DVf@s|IC)xbqZtob@qJAYQR8wm&$6dbGrc1=TS@F zZ1y^@M@YFowg!L-r@~}fqxxCSy9)AO^|8TYgA&)ijL+L_C{;Dha3Su7dc(IkbunT} zKaHzbxx5;?uJ*1EDP;q9?uEt?TJ>p%D!&P7I`c9sv8kzvIylF{eP&ox*&@3}`C;m@ zY@lc9PXx8E++25wmD9uYM_`+bM#6Vg9PA-fFv zsjs036&!DZT1KHi8TVJ-Skd8GAIty#&`CTTrKO9pd95X897QtfN{9;i^bv|ns@jcF zu8f+|HK0iH=ettb_TDJ-jD0oN$v~L>8~U-*8&qA$D_Ibrk=Y<>VWiA@@mP$(I)K^; z;^*-e(V-%DT~;|ncOl=HKI1f%EDZ%$acMWqQcorsg{hx|6n0GDj%gvei>5|UVT)#6!OrR_; zV)Cw&^c?Z;*Jt&`vGuSdnUGvdofAdyuXpd1EHTy^bAm&ASRP#`_BhYvRJrMbGb*~i zUEC`fQUsSmWf;UbDy_DTuzEDzE&9c}a`iirLUtOM%f=NblRe9s`0)&@KgQRyObhR< z=>nSl4Qc!ud-tNDhx>u-_9CS1I~>hfk2e{2d*mGixIjDF4XsjcrtEy)!wzeZylI%1 zQuWN8Cj8cEOOEx?_b&^Vy_DNs*37Jy?w}KOa=*$(_h4lX4gN$4$R9GLvf| z`{Cvnp`~Gql@Df0w-QwpdL3L{IUU0XQdag9tOsjLW3eW&D3;<(HAiMUi)7k zULQ^zo=4qH>6EfpWoyESMha9)>qz%KXb-pL}hxBJh&?J>mOY= z>OG1$iW($MI?Ug|g>P}a0?h;(JF(EET&k=U$Rs5 zTDP9feW)z(Au7nfM2;oX$8VAIe}Dw!Yz=`}-D#9V)u{%XF%CR-W~A?;U$xf=N>?6E z{9dM_G#;cag*^%+QA!f1e5KA&y*cw4x4GrzS$66?;Ti&f89izEb_p5O?n*1!M5n0G z-JF1G`Vejfxxzx}6Ixu=DQr67Rp^MZ0&Foiy4CJcRUrGpqN4G+@3$lN*FUq=caZgz z?3QG!Q6FvSO9PrMT3m-x@f%Ug6`vl@%UaamX{+PezYt~j+L4P3vs+i2#DA>oncRZl zyN+LHaX4K>7N7ORW(|_kS!&fPk|3JdgL64>Y6raks{e1vd+hmK!!xCrxqR>21M5sh z%`$o7Q?u}QAz9=McbYNGjvBE$`hPDl*}KzCM4t+};kJ)-db4?kfu3BA9q&pJ;Fuu%K@SVa>u&Gz{XXq5R? z5Hl2e_7z&^;SNmbzQ!T4uc2zx10gLsovo#R=-nt8T zVMRG4Aw4YB^Ox>Pxc?maSJ#TxgYSN*c`knduh;5z_)QR$&pkNjd-7;={|JeSV#!#0 z{Rmut*{7X|a2?*06{_cb6R5Jz^2+P+h9^GF@ojrNys(@GsoTC3A^7XkbOfUgsGv#; z$Jn;~Aa|k@uMX7i(xiLyiceA+dNhx!iRP71cr!lK+PA^nKFFy(qQO38}usWT6`*wAzu$v)Xi8=9l`vgN5i8XI-SXq63 zs8sIzqENgDD{LCZ<>Y7+rlyi+e}v?Tk$GI3ppEGzAKuSEc&>ucgAaa z7-7=#9MTQLrR6})vs_tT!dncix@`)O*P-Bf5}j7tyNo z_zTKEN>KP`LuJ;hva)eglZtBgxWm3Rys#Hv#?M?*{(4o%SzDgL3R89BWRFx8+?pRx^v{(-yB;Gvq=YH1ypi6P-*pP{%xS=xDl?hI z&mh*_Q$b$Wx-4_~wgnZt84G@mWe>VCH3Uk)=Ro58tGk}-^5GB3FilpFe+g0c1^xlg z?FiUQ#FIywcO{j;ww;G5b|=u?QZuTG>%;b@#+Hm(3HBxae00}g!h6+l`@7lDMj3d6 zxWl!8HrAn69-*(cJ0V`0$Y}Qxd3{w%bRwW$Sr26HtY7ATJArG4d2lXpz7(@7X!7x@ z7YTEB52h-FrVcscyCC%rJJu`uY3bYW!tyM<&5*wI9+zxde)q)6lX@KA^QvfdnK(B< zGM#2pS$$RONwA9PDcexQ#&3LgJT?2o+oSp3uKLnPn`-`q4eFx57Ya4^UiSSvLoAR3^e-jL;rlv-&Q~5UB zsum0=Q*G$DhF&DR(EN^ae{_FYrHFgTsb*~{aMb-^?saJZNf>8IS`hluJjY8{M}A*r zN}}PDYtQF(2393({Ai2kVR`j=@K@sD8(Z4XH5S6eZ?ct<#RlLKi(aXpr9N-=?<`WF z>3sFJMDB1hI^U|e>{WMSo@DT|3!Yy@1W7~jqy`*Ln7!OHmS^h56x1g& z+Ztq|wK2`E2EwN^-^)ba^yV6hClwPulZ-dI^iE-{5FhQc^F8f|X9|95b)o0v5-}ib z`?=_Pa?y9x>j4ZKM@;(HgWRN*Wj#vPi8>BG+fdrdX~epp<1%}3yLD8~BdeO{Ua#^*FFX$N#^MoJcuz5q0yZdr>GdDGT! z8o~G6f0^#9O|R@bh&~-BMZK6~GxAXE9>;B7PII;l_N1CCi_RiJKN8>r$!~w3KCKgi zSvC{2;_7RPBkEh&gK-r5?RT~+#cN+kONRk|;iSo%N3%-QTKO$>KKw(^PxDAX5LBll zx(k~>`0?k;8i&M%!}hu_OI(W>=T{7(`ueC%{QgjXJ|Q0A+bpqe%CzSaDy|lFNJd}F zn1Bab5^BNie}t5}{2i^TxjEvSQs+rqX5NOwORkzd@Hw%dtL!}XX<68(H71b@*13Fk zX@5mX=4E;C<~TN<_yZP!roG9y<4pJGm#+(ofCyG~jLddjz25QR;5gjiv^Y2!X$;xI zdpB^x-xfskw-@^vX#cOyw?zLBog@r^>ywr%6Y2kKHTjfygp~hkrBeU*UqwwJzQIQa8K&_3Eu^X67hAPy&vp%`}dTzEpQKv*#Hkiacfyn>y&l^7?P9E)y*2a!n( zJ7XWAHn&O+CMMX&1+L;@MdX3}Pdogkv)4Z$_2~{p#*9y9)?Qy+qDQECz!465K~A;D z&hE@LLbf48G*iBB5aEc_`ku0K;JdBti(d4URg&tnvutjcUFkUhR}2jf9$|Dcus-J5 zty8*1{Qqu69_heOrUF{CCkQp1@K#ySi$zK^{c{1Rje_l}vw5p+&U+>#DGOz^J8YUd zyrW%vSfsZ-v;Zi2$yEzH2u8Q}`h#6~TC&9d`z?44#tL!o-Vk{^v)HX$)ddKV>eO{# zQPLzh7|elyqn}dL(4Ng}CyaZR&RcVB=GgSy zzx~ZFD#ke&*>osa-*%k9x7$6N^OuP*P!ZVV96K{*2vw53rUGx~Hh%dEo_Vmlgbp}y zg;tb)GM@f^$V;tXL-ppugkk3LY7G30tmi7ZiIy6$#ZXaR9hR)xI}}9qFR8ysG{fmB z3Yj5p*SjCIZ5kx8Q;3q7Th0XBJo71}Uu7_MVUDlOmOBEuRRaL^=6ZRMT)QRA5zEuw z24HQ?A9m%q9W2gXSFh0^`LAIwnbwhH0xO~vN*s1^q`?HiOGEOml0iV`c)ndOkbpW3 zeBw19=!^2st%lnB?wpI#G=QbZ(+EOm>ms(moU`*)B9q?UN)HA+SrQ>etZ|^mrMI%! zZ+i!Z2{QrqzFrBLnb!B-Wc^8!h@wvzLb0R^>vFk;nYP$}7OMsEw$hdtJLR*r{;N+r zb2imo9lO4WpLYS8Xs7SfZjvJsr{7=Gv~y%NoZ&+}ancr+U+PSl*a5%okP^YK|1uclxu1a%&oWw6tTZ5nW?~LiQRw*7XeFk2?1rhF!!8F?*-OfN zx`nvk;!ys_nX2IOBh*=j(-$YfJm(7r=+q#cOQIk=uol&ij8AlKjB`2MZdwdI*w0Yb zHeUOSXz4^ml^@Biq*U2Dm}LlyUqZ?zmVG_BIHYfk0upSaT|MXRrqKi7YTYBTtgO zV8G)Sl?@*Kh~xt0?%xKi5Ddtm+6=s^((Bi!4}Uh(!q@TM2`^5~wD)qZ+P8j^U7FH@ z#P{Ks=U5Mgr^>LHIMafgBO0Oqx#k~wtR4;Fg;ku>xH`fM(%Z;wkEY9$>sVjbsKTqW zXaHk@@Q1?CBN_l(t!9nskHu7zPlOw#M?1(+c)j;b2PzCnKlyr;)453ciXoZsQb+kqU__5ex1bGY6n^;-#3e0YtxahynnCtKbG}c z`M&#N#?n?;@MN{nerD2Nyh>1fL%0>!a>)v8C*KI+_O$#vtd{`)3#KYrfXkl)oh8N}bdkSmU4-FhH+WF0eq3o^Z=!Ybaf52jr zgtx<*Csf3joQc2Tp5gr4GNuCmV8^f`#EyY5qX+@p!@srkE~v`(tl4>1omV>|ycPD;?iEyw{ga{@^EbN3mYhL3#xL=GWw<40v8GJz5ha>-?y-71lzHfvho~vf%SF`_O{@_SWEuDbq;$+|BpT~ZA=6= z#LM}Pe>kvq9|tIAZww@sMDyMtK!1H`r+eFSwAsw{hLp)TGx|9STq`ce44U`slj7Xh zX9J5=@3|(T=TZL9Ms8Kt_KQ27-dg3;TCPK^I~|{G+{ap=ov8laBgL9_mE@ezT@6B0 z50{FPmby43!&SwRypsM!LU_5x&Z_IcEx!OePdPJrw-hYbcK9~A%T4;g>V~0`i;~-a zUG9;_rtR#74Nw|>c~pZ@tHRNb`Srl3zNkSPFN&6Ru%(@c{S_CidaR`?6)EjfE>nVe ziZ++R+Rqz~ishQKubGu^`K7kY;*zYHFGjcqi7we0{rzb=mrXp9Vb1loEOqmH>#zM% z?r<#qU?(ES(#8K$+qxm-A?I=A)Ulh1^{;9vuWBT*W%amb0^oY)Ca@6<9GfbK^TtrU zq3~Z??bCxr^)d#dJeOV%Vcp;WEQaQca`Qa4L!Xz83mG_J3) zzSS!?%L~17Sd{4H;TBgBYU*+2GSBW>woTu*UP8?AYCUFW&^_Idza+Mt;Fg=f z2c{gS#|`rS`u2}n>GaWUK#{W`sQy>^8GukHF;{1#yHbup$E%E4emP}C6t+!KLaA~dF(Mj-yuk$%4bdP z)~`(V#Gsfi#24}!O|1Lfe@@(05@qMsbHQ2V5f>ppawPJJv44*T5m!edd1~)X@S)RsoP-!_ z1VP6}b!l%{CFHvdXd#@P*q1sH6tkA!4rN!|Wq5gAtauRyS$CXhCG%Jt##uw3G zh<88hh&OH@uf@I+W544i?p!?t>b>0aBD9E6O8G64N}1V`=iVA@Z2x50nbGYk_+Tsd z*OuO)UzxijQp#?v+$@9lov`Svld-g&EkzYVkQ;$%}1>J@A8P1GJ*adOU zqx4Pbkyi@-?S@?uKF~O6YFLZRrX%cuQMrnDcy#ZotbaUv9&jXpmE(n0;! z*MwIqkb;310S_2%&vVrx8_BXep$3FAFY-$2^vJAZR(d!dB-D}Lk#{t-Gc!jo2f|gA zmY-O?rxUuXS{KPtO}Ub$@Fy)AYQBFSMU&HFCLO8)nm^k9{pTT(oT_!zOMh{X6)h;2IrE002yukUCtIPYx$|75eD|I7*-!6onf6E@zBqH z6rU9G6;Fg@6@M~#NL*nI{iHK0;_e7Gc;!=vq#0;C!ix}K#n!O!(c>NtoyfWB$)AI!kyYe!+` zjV&^-bj%DaMO9~Q%v$`r_ZPU~{T5rejqFe1v0~n%vxBApU+zS>b%g`RLe+{=cIn91ZMy+=~_FXP8zGe z5$x^@nQJyPQCqu4f_!OAR=Nq2$*ZUVWwJjP&gBUrQ~1De3W7-aZUHhMCX&e#DTov{ z^zY0khy++f3;Ps?SREfi{UIqWl)+DV3y>?#&u`;x{n7SEL}0{1jF-wn2G-f}mH0s- zdzI8ni~XM!pn0&Ud9QD}i%xsPx`?Mt28Pmzi4H0by=FnSPH@+gp)oQ@35^g6T#hV=lF zFcOoGz+*BQX5ChTU?Gu$$m z?(rS?+27hcR`%6{u?mtXCYyk_5&l(@Eaf_ox=a2aR;~{e;WA2rv})^c$GLgCsUg1@ z#?p7U(|ZHI=|V_807~cFjV3?oThtB8KG@k#&uA?BI7xCyp3&q_TD|Vzl0#uA=5WsK zLltbrXUEBY8-Ul7acu7TGYt5fSg18bA|jZq7{o_IQO_B1k?rvSB!Fu3D#WeC@W>Dr zIcqU0Z-S>TxNMpRxIZKA59`dufR22=bFIrqvWT07(cPELrxaQ$mM5|{_U>HUWR5~o zthJ#h2%U|?a zI}^WGJi2pBx2t&Q%77y@ta5tr?G=&VQf;q1nct>Y=w;4mpll-W3Fcc)iKW|a^7;c3 zIsh-qMLp==y{#xWtOICFpi}GZ_AXOe_u$?<^AdrMEti{Ol>6dg!c+I@5VW@>?uPjk48=626|0Z&u_V6j(Q<_Q&?{?$!BoTh&Ea05%Svd3q1TPs(_ek7=rw;|vm3x;`GkUG=JqdHg$UVIn zvZDbUzHox(`ghnXOG|FCzWh7c9_r;A+s__+mjv+ajMdftDHVM(qKHNCP`0x7h%*+} z_&inU8|X1NwED-A?S0<|JpjyD7Y$lX2dwa&*%-^I^kl+ zms$r=CNTtVT9G6juK7otGjdq3Kr`#Kl^u1oigC!)t@Kxc^ivwK60Su7ebHlc*~{T> z*sT6=0`O0vOhc5qSvZ}4a+v!wXq$#!wePfrzfq)VaQl22Pe5dW^htpMHnGf@TJve! zE@Ji1)&^ex+hmPH^#n}p7!MGHRIkXFl$wDH|A);na{NuAtl506ty?r>HyXJ|VP z!C`rvL5(SmbSeL|GJmjP!+GTBU&a<6O9v%axn_wW)Om~bz(-NJcH8v@hdFm>0FMLT zoqXDMExz8y(Ejy+9~t--AEh;72(d>~A41~D(=~19#hTI`MGwFeqY+a!^+}Jdm!TKr zx3w0A$?=!)X#WiG)lAYhbQ5Z>MpHtCREg$4V6BWYq0XP>7bH=u{3ivnf1zcDz9%eY zFM65Tmz!hyOp9d}_N?KSm!)3|D{+ID#H{eZ-~U`IIRaTQWk2U@n!$H4ORzc{eYJ!- zgJlE%qS-4U-)qy>!yL^6$8FbI-5&?1-~&g$*PO6EtT&%)6hz{!c07`2tGu1)>F%>& zF?3gqpRWkXk`tKx4XNjCJ;LUO%l$>LwDe z07&<(7RJ-yQO$EXZ9vWGMb`1ORx=jbZWEPhzJBm-^Oe7{`(?J2g=>YdE3$N_?=)tz zONmLyX6$cDa)Qwls;PrD*4NYC&NNo<53x9&Q`2WiQum9hMjsNGIed`_2 z@V~WLM=Y22-@Xfs>Mk;f|IduZ{|{PL+0XyY!>mY8XO^wLDZbYT*y%grhnj)=;3@Vh zR)Hy;_^hm8|5X)PljE8Bn`)?YLCTGrB27&FsKpH~e$srY0fXTEP{3B>SkCo$4jmpX zVEF@7%E+r)Td3cw$Ed*9q1ZH-%?&c^npQVZ*5Vu9bXoeWK4#DJ9E+XbRK8QW*jG#a zP16Nji9r=AL9=EkAJ?gusLmnrYW-D{%sJ&=loMPLzO%fuG4NA^8LaXjT!byVeo_HW zFTD%rP=8O)Y;@Qq$H@Drz5Tp{yzry;^>NfcxyVGuGB~EZrB^J=FI;qYgEFu>V(9)| zW;lNZ?j9^Tal$k!9a}ASwLxP>Nss%H}tuiaIBucz7IBXBH;_DaY4pQ=9#S4Ok5)rpsueQM8k$}4~M2T4PhLnp(C zYjfKSAp1-{YYU={5aqU%a?JezQwFmzjK!rhOO8pU{E|LwW_H_>xzZ;PNv9-6w5WGi za#(qHc0*3)>$0%Ryqii-53WsPSxcA2pY>Vz4N@0RTzB0Ji@S&FQW`za4E;Sj7}h}) z8$hdGzR6?~?Q9J9&x)(ZR>_TRl;0b=6}>`!P=DC4SC!|~F@#LIGK;KzO$`V>pX&V! zL9$$QD?cc{+CwU~4W38R`Qt%94}FZ?oBz;DXhQ>Nu2%1r{0NWHhZMG`Vk2F_bF~DX zKMvKV-Lik(GbJ{Dkm2%u3~a~&&ps3He)aMWFIfB0w}D~WB8AubEH??h`d)ve@YtbT26ZIy0*b`sji zd5`F!0x+GNbpk<5VWRvc*V;ikpqFQ}WUY>-2kV6zs7O*Xc$FS!2$}kI#pq|ob(Zw^ zr6As9o2U!CRuVL3Jv%lFC1v{_XX;*~EL%dIkVvia&e*5%2t3pg*mb17hQ}{`tq4GW zH>r-JDNj+%e=;Y`s@l&%yYw?+h$wc?JPf-MSn{yW{M&~1I)dIsSPw6#cj(0`@m4~a2S!gf zajPyDm^5KE@QomWD6x{bWXt;~$2+LV` zs}L*B8Wt_3Wahn|Ms3U4KpO9rH*JvkfVk2pD&u!YR9Z;~T0HYbwK@T|%cEsV$^ywB zX>mXL9&$LaD{!WIwbl6gpX;8-!2W7Dc3+X?N%N}K>yfR0u07xV(2M+l3kb;G zBm{CzYw2cK`*zUqPQ><5BsMWGzYt`zGT#2s^OamI8fA$kbXAP7p7HfV?T=N1L6|RX>f| z-io}f>}4Kv`zTRV_Xy$Y$Rp(TRi36?B@DWLe*f+r_)J8tTB!P1pPV1rh~lxVtqRD* zjtlf49yY>^bfCWEjfW=VxxIN;TLdI!M>LQplXr807Ob4Y#=_Odv(I#TXb`t?k3PK& zR(^+fpP#6*JRZ1J1^nFh1Z=PECqznzV0K=6rs>?mPOgj%u1Gk*{t1a&eZ~I8p(a`H zt+P0u!Uvs#*rKLA&OzPFpg zy7@qK@6}xDyM)t$BY{y&8_~n+c@#B=rk@5?Zw2$}s(hDwd zWgN9fs$pq0ZK0(@kUX&A_26HSemc{eV=D~`UR(4j%mQESe5si_j-T~Z!GE=?#M^g2 z($?BC!cuJXPKaMr;y3(_vozhgf?YWTMn-d?Oxit4^iK4|0azfI#tGl3!COUjMMhy{ zCGBkbd~(5^5c(ZaE8+$IV&(Yzl;ws<^an32@>yJ3%F@_b*6m<`!xu|`u)wK;T7zux zUmNN_Q)!JO`!@6L50U5S_rb~&cm?P98!lg;1U|H2#I>OYJV&H@mgjMX7UVcnT(r#p zy?+RAD^I(7yIEVVYRuaRUyLNR8sa6%vemdmv?t|5&1r+HPCjc*W6XBT#kQ4A`&1uL zKQ(Fev!7Y9nred+3eb8*pa)EAGcA@TP|s5|9nn-Rh8}NI9zu`VlXzawI=B0(vGlYtbI3nZY8u( z&P}$o=ch&2dO~35LlrqiVcwh%&1wPOZq}vn{Mrt@h{JdikkIkkFaQcT`RK0k-JT9% zSlrplnukKyqp?-;pRQbl;a6F%)#fV{fozGDS?O1 zcL}v}fj<7K6FAE3?Q4ih)G^z&wAN=%p@c@|-~_jyQp+}b(F!p}>q96Kl&*EX2$+FO zM}LC8WB^BC<**Aks3@p2q3c59BvO)uj_S4vK?xm;EiJ63@mESpz{>!jg_=cHItBJP z-{uDqC`XhwcRn^*kPy9G`i^~Rk)Foq0wQ6wGubhPK{i&tA)IFcJ^q~Ur-5cqUABc> zu*w#X`&L<3VD~91!}ZBZ&wg0gEw@~zXZiX4ZC!o-yjVWk@99BJ2;~J979F#7aXxle zX&V@ad29cL?ItW~wy^IW3BC)wh(*_Pby0er58zKQIt^jpj}&;5b9}k4wod;?8K_l{mB7I6^kEsj;&v z0Wvdb`dKio>UUT-S?7~r->8xRrvohA-F^wJsd?p~`9In_�!_ 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. @@ -22,10 +19,9 @@ Comprehensive examples of BrainPy please see :caption: Tutorials tutorials/installation - tutorials/quick_start - tutorials/build_neurons - tutorials/build_synapses - tutorials/neuron_analysis + tutorials/numerical_solvers + tutorials/neurodynamics_simulation + tutorials/neurodynamics_analysis .. toctree:: diff --git a/docs/tutorials/neuron_analysis.ipynb b/docs/tutorials/neurodynamics_analysis.ipynb similarity index 99% rename from docs/tutorials/neuron_analysis.ipynb rename to docs/tutorials/neurodynamics_analysis.ipynb index cecd2db9..8c62a3c9 100644 --- a/docs/tutorials/neuron_analysis.ipynb +++ b/docs/tutorials/neurodynamics_analysis.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Neuron Model Analysis" + "# Neurodynamics Analysis" ] }, { @@ -748,6 +748,7 @@ } ], "metadata": { + "hide_input": false, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -763,7 +764,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.9" }, "toc": { "base_numbering": 1, @@ -781,7 +782,36 @@ "width": "243.07px" }, "toc_section_display": true, - "toc_window_display": true + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false } }, "nbformat": 4, diff --git a/docs/tutorials/neurodynamics_simulation.ipynb b/docs/tutorials/neurodynamics_simulation.ipynb new file mode 100644 index 00000000..259147c9 --- /dev/null +++ b/docs/tutorials/neurodynamics_simulation.ipynb @@ -0,0 +1,108 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "overall-angola", + "metadata": {}, + "source": [ + "# Neurodynamics Simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "union-infrared", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "corporate-trunk", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "photographic-acrylic", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "biological-tsunami", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/numerical_solvers.ipynb b/docs/tutorials/numerical_solvers.ipynb new file mode 100644 index 00000000..2a9d4e1c --- /dev/null +++ b/docs/tutorials/numerical_solvers.ipynb @@ -0,0 +1,605 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "premium-shield", + "metadata": {}, + "source": [ + "# Numerical Solvers" + ] + }, + { + "cell_type": "markdown", + "id": "destroyed-smooth", + "metadata": {}, + "source": [ + "Brain modeling toolkit provided in BrainPy is focused on **differential equations**. How to solve differential equations is the essence of the neurodynamics simulation. The exact algebraic solutions are only available for low-order differential equations. For the coupled high-dimensional non-linear brain dynamical systems, we need to resort to using numerical methods for solving such differential equations. In this section, I will illustrate how to define ordinary differential quations (ODEs), stochastic differential equations (SDEs), and how to define the numerical integration methods in BrainPy for these difined DEs." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "specialized-wyoming", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:11:10.299805Z", + "start_time": "2021-03-23T15:11:08.620690Z" + } + }, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('../../')\n", + "\n", + "import brainpy as bp" + ] + }, + { + "cell_type": "markdown", + "id": "opposite-mixer", + "metadata": {}, + "source": [ + "## ODEs" + ] + }, + { + "cell_type": "markdown", + "id": "complicated-italy", + "metadata": {}, + "source": [ + "### How to define an ODE function?" + ] + }, + { + "cell_type": "markdown", + "id": "removed-championship", + "metadata": {}, + "source": [ + "BrainPy provides a convenient and intuitive way to define ODE systems. For the ODE\n", + "\n", + "$$\n", + "{dx \\over dt} = f_1(x, t, y, p_1)\\\\\n", + "{dy \\over dt} = f_2(y, t, x, p_2)\n", + "$$\n", + "\n", + "we can define this system as a Python function: " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "failing-headset", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T14:10:54.574024Z", + "start_time": "2021-03-23T14:10:54.545736Z" + } + }, + "outputs": [], + "source": [ + "def diff(x, y, t, p1, p2):\n", + " dx = f1(x, t, y, p1)\n", + " dy = g1(y, t, x, p2)\n", + " return dx, dy" + ] + }, + { + "cell_type": "markdown", + "id": "devoted-television", + "metadata": {}, + "source": [ + "where `t` denotes the current time, `p1` and `p2` which after the `t` are represented as parameters needed in this system, and `x` and `y` passed before `t` denotes the dynamical variables. In the function body, the derivative for each variable can be customized by the user need `f1` and `f2`. Finally, we return the corresponding derivatives `dx` and `dy` with the order the same as the variables in the function arguments.\n", + "\n", + "For each variable `x` or `y`, it can be a scalar (`var_type = bp.SCALAR_VAR`), a vector/matrix (`var_type = bp.POPU_VAR`), or a system (`var_type = bp.SYSTEM_VAR`). Here, the \"system\" means that the argument `x` denotes an array of vairables. Take the above example as the demonstration again, we can redefine it as:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "historical-chapel", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:30:04.376593Z", + "start_time": "2021-03-23T15:30:04.368594Z" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "def diff(xy, t, p1, p2):\n", + " x, y = xy\n", + " dx = f1(x, t, y, p1)\n", + " dy = g1(y, t, x, p2)\n", + " return np.array([dx, dy])" + ] + }, + { + "cell_type": "markdown", + "id": "considered-surgery", + "metadata": {}, + "source": [ + "### How to define the numerical integration for ODEs?" + ] + }, + { + "cell_type": "markdown", + "id": "mysterious-holiday", + "metadata": {}, + "source": [ + "After the definition of ODE functions, the numerical integration of these functions are very easy in BrainPy. We just need put a decorator (`bp.odeint`). " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "apparent-structure", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:07:37.736843Z", + "start_time": "2021-03-23T15:07:37.716840Z" + } + }, + "outputs": [], + "source": [ + "@bp.odeint\n", + "def diff(x, y, t, p1, p2):\n", + " dx = f1(x, t, y, p1)\n", + " dy = g1(y, t, x, p2)\n", + " return dx, dy" + ] + }, + { + "cell_type": "markdown", + "id": "meaning-print", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:04:17.122194Z", + "start_time": "2021-03-23T15:04:15.983197Z" + } + }, + "source": [ + "`bp.odeint` receives \"method\", \"dt\" etc. specification. By providing \"method\", user can specify the numerical methods to integrate the ODE functions. The supported ODE method can be found by" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "artificial-curtis", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:11:22.057190Z", + "start_time": "2021-03-23T15:11:22.047195Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['bs',\n", + " 'ck',\n", + " 'euler',\n", + " 'heun2',\n", + " 'heun3',\n", + " 'heun_euler',\n", + " 'midpoint',\n", + " 'ralston2',\n", + " 'ralston3',\n", + " 'ralston4',\n", + " 'rk2',\n", + " 'rk3',\n", + " 'rk4',\n", + " 'rk4_38rule',\n", + " 'rkdp',\n", + " 'rkf12',\n", + " 'rkf45',\n", + " 'ssprk3']" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bp.integrators.SUPPORTED_ODE_METHODS" + ] + }, + { + "cell_type": "markdown", + "id": "surface-brush", + "metadata": {}, + "source": [ + "Moreover, \"dt\" is a float which denotes the numerical integration precision. Here, for the above ODE function, we can define a four-order Runge-Kutta method for it:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bronze-sport", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:13:54.738449Z", + "start_time": "2021-03-23T15:13:54.729900Z" + } + }, + "outputs": [], + "source": [ + "@bp.odeint(method='rk4', dt=0.01)\n", + "def diff(x, y, t, p1, p2):\n", + " dx = f1(x, t, y, p1)\n", + " dy = g1(y, t, x, p2)\n", + " return dx, dy" + ] + }, + { + "cell_type": "markdown", + "id": "chubby-timing", + "metadata": {}, + "source": [ + "### Example 1: FitzHugh–Nagumo model" + ] + }, + { + "cell_type": "markdown", + "id": "christian-receipt", + "metadata": {}, + "source": [ + "Here, let's take the well known [FitzHugh–Nagumo model](https://en.wikipedia.org/wiki/FitzHugh%E2%80%93Nagumo_model) as an exmaple to illustrate how to define ODE solvers for brain modeling. The FitzHugh–Nagumo model (FHN) model has two dynamical variables, which are governed by the following equations:\n", + "\n", + "$$\n", + "\\begin{align}\n", + "\\tau {\\dot {w}}&=v+a-bw\\\\\n", + "{\\dot {v}} &=v-{\\frac {v^{3}}{3}}-w+I_{\\rm {ext}} \\\\\n", + "\\end{align}\n", + "$$\n", + "\n", + "For this FHN model, we can code it in BrainPy like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "saved-participation", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:29:53.081577Z", + "start_time": "2021-03-23T15:29:53.073575Z" + } + }, + "outputs": [], + "source": [ + "@bp.odeint(dt=0.01)\n", + "def integral(V, w, t, Iext, a, b, tau):\n", + " dw = (V + a - b * w) / tau\n", + " dV = V - V * V * V / 3 - w + Iext\n", + " return dV, dw" + ] + }, + { + "cell_type": "markdown", + "id": "freelance-carpet", + "metadata": {}, + "source": [ + "After defining the numerical solver, the solution of the ODE system in the given times can be easily solved. For example, for the given parameters," + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "annual-wrestling", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:37:54.696961Z", + "start_time": "2021-03-23T15:37:54.678955Z" + } + }, + "outputs": [], + "source": [ + "dt = 0.01; a=0.7; b=0.8; tau=12.5; Iext=1." + ] + }, + { + "cell_type": "markdown", + "id": "competitive-transition", + "metadata": {}, + "source": [ + "the solution of the FHN model between 0 and 100 ms can be approximated by " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "dated-sunset", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:37:57.984245Z", + "start_time": "2021-03-23T15:37:57.736440Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "hist_times = np.arange(0, 100, 0.1)\n", + "hist_V = []\n", + "V, w = 0., 0.\n", + "for t in hist_times:\n", + " V, w = integral(V, w, t, Iext, a, b, tau)\n", + " hist_V.append(V)\n", + "\n", + "import matplotlib.pyplot as plt\n", + "plt.plot(hist_times, hist_V)" + ] + }, + { + "cell_type": "markdown", + "id": "acute-prototype", + "metadata": {}, + "source": [ + "### Example 2: Hodgkin–Huxley model" + ] + }, + { + "cell_type": "markdown", + "id": "whole-mother", + "metadata": {}, + "source": [ + "Another more complex example is the classical [Hodgkin–Huxley neuron model](https://en.wikipedia.org/wiki/Hodgkin%E2%80%93Huxley_model). In HH model, four dynamical variables (`V, m, n, h`) are used for modeling the initiation and propagration of the action potential. Specificaly, they are governed by the following equations:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "C_{m} \\frac{d V}{d t} &=-\\bar{g}_{\\mathrm{K}} n^{4}\\left(V-V_{K}\\right)- \\bar{g}_{\\mathrm{Na}} m^{3} h\\left(V-V_{N a}\\right)-\\bar{g}_{l}\\left(V-V_{l}\\right)+I_{s y n} \\\\\n", + "\\frac{d m}{d t} &=\\alpha_{m}(V)(1-m)-\\beta_{m}(V) m \\\\\n", + "\\frac{d h}{d t} &=\\alpha_{h}(V)(1-h)-\\beta_{h}(V) h \\\\\n", + "\\frac{d n}{d t} &=\\alpha_{n}(V)(1-n)-\\beta_{n}(V) n\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "In BrainPy, such dynamical system can be coded as:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "sexual-butler", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:42:17.921752Z", + "start_time": "2021-03-23T15:42:17.896456Z" + } + }, + "outputs": [], + "source": [ + "@bp.odeint(method='rk4', dt=0.01)\n", + "def integral(V, m, h, n, t, Iext, gNa, ENa, gK, EK, gL, EL, C):\n", + " alpha = 0.1 * (V + 40) / (1 - np.exp(-(V + 40) / 10))\n", + " beta = 4.0 * np.exp(-(V + 65) / 18)\n", + " dmdt = alpha * (1 - m) - beta * m\n", + "\n", + " alpha = 0.07 * np.exp(-(V + 65) / 20.)\n", + " beta = 1 / (1 + np.exp(-(V + 35) / 10))\n", + " dhdt = alpha * (1 - h) - beta * h\n", + "\n", + " alpha = 0.01 * (V + 55) / (1 - np.exp(-(V + 55) / 10))\n", + " beta = 0.125 * np.exp(-(V + 65) / 80)\n", + " dndt = alpha * (1 - n) - beta * n\n", + "\n", + " I_Na = (gNa * m ** 3.0 * h) * (V - ENa)\n", + " I_K = (gK * n ** 4.0) * (V - EK)\n", + " I_leak = gL * (V - EL)\n", + " dVdt = (- I_Na - I_K - I_leak + Iext) / C\n", + "\n", + " return dVdt, dmdt, dhdt, dndt" + ] + }, + { + "cell_type": "markdown", + "id": "subjective-formation", + "metadata": {}, + "source": [ + "Same as the FHN model, we can also integrate the HH model in the given parameters and time interval:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "worthy-restriction", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:48:24.927865Z", + "start_time": "2021-03-23T15:48:24.919865Z" + } + }, + "outputs": [], + "source": [ + "Iext=10.; ENa=50.; EK=-77.; EL=-54.387\n", + "C=1.0; gNa=120.; gK=36.; gL=0.03" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "regular-kernel", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-23T15:48:27.412192Z", + "start_time": "2021-03-23T15:48:27.019017Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "hist_times = np.arange(0, 100, 0.1)\n", + "hist_V, hist_m, hist_h, hist_n = [], [], [], []\n", + "V, m, h, n = 0., 0., 0., 0.\n", + "for t in hist_times:\n", + " V, m, h, n = integral(V, m, h, n, t, Iext, gNa, ENa, gK, EK, gL, EL, C)\n", + " hist_V.append(V)\n", + " hist_m.append(m)\n", + " hist_h.append(h)\n", + " hist_n.append(n)\n", + "\n", + "\n", + "plt.subplot(211)\n", + "plt.plot(hist_times, hist_V, label='V')\n", + "plt.legend()\n", + "plt.subplot(212)\n", + "plt.plot(hist_times, hist_m, label='m')\n", + "plt.plot(hist_times, hist_h, label='h')\n", + "plt.plot(hist_times, hist_n, label='n')\n", + "plt.legend()" + ] + }, + { + "cell_type": "markdown", + "id": "corrected-cream", + "metadata": {}, + "source": [ + "## SDEs" + ] + }, + { + "cell_type": "markdown", + "id": "incoming-result", + "metadata": {}, + "source": [ + "### How to define a SDE function?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "stopped-finding", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "competitive-attack", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "short-explanation", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + }, + "toc": { + "base_numbering": 1, + "nav_menu": { + "height": "198px", + "width": "397px" + }, + "number_sections": false, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} -- 2.34.1 From b3688bd8b4791178202232a4acaf3e4ef30b8b9b Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Thu, 25 Mar 2021 11:11:07 +0800 Subject: [PATCH 14/15] Update doc about tutorials --- README.md | 2 +- brainpy/analysis/bifurcation.py | 11 +- brainpy/backend/__init__.py | 10 +- brainpy/backend/operators/bk_numpy.py | 8 +- brainpy/backend/runners/general_runner.py | 2 +- brainpy/simulation/dynamic_system.py | 8 +- develop/benchmark/COBA/COBA_brainpy.py | 10 +- docs/apis/analysis.rst | 4 +- docs/index.rst | 13 +- docs/tutorials/build_neurons.ipynb | 4 +- docs/tutorials/neurodynamics_analysis.ipynb | 456 +++----- docs/tutorials/neurodynamics_simulation.ipynb | 1037 ++++++++++++++++- docs/tutorials/numerical_solvers.ipynb | 587 +++++++++- docs/tutorials/quick_start.ipynb | 2 +- examples/networks/Wu_2008_CANN.py | 138 +++ examples/neurons/HindmarshRose_model.py | 38 + examples/synapses/AMPA_synapse.py | 6 +- 17 files changed, 1908 insertions(+), 428 deletions(-) create mode 100644 examples/networks/Wu_2008_CANN.py create mode 100644 examples/neurons/HindmarshRose_model.py diff --git a/README.md b/README.md index 9a38c728..019fcc65 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ``BrainPy`` is an integrative framework for computational neuroscience and brain-inspired computation. Three core functions are provided in `BrainPy`: - *General numerical solvers* for ODEs and SDEs (future will support DDEs and FDEs). -- *Neurodynamics simulation tools* for brain objects, such like neurons, synapses and networks (future wille support soma and dendrites). +- *Neurodynamics simulation tools* for brain objects, such like neurons, synapses and networks (future will support soma and dendrites). - *Neurodynamics analysis tools* for differential equations, including phase plane analysis and bifurcation analysis (future will support continuation analysis and sensitive analysis). Moreover, `BrainPy` can effectively satisfy your basic requirements: 1. *Easy to learn and use*, because it is only based on Python language and has little dependency requirements; 2. *Highly flexible and transparent*, because it endows the users with the fully data/logic flow control; 3. *Simulation can be guided with the analysis*, because the same code in BrainPy can not only be used for simulation, but also for dynamics analysis; 4. *Efficient running speed*, because BrainPy is compatitable with the latest JIT compilers (or any other computing backend you prefer). diff --git a/brainpy/analysis/bifurcation.py b/brainpy/analysis/bifurcation.py index 64fbb6e2..4ea71000 100644 --- a/brainpy/analysis/bifurcation.py +++ b/brainpy/analysis/bifurcation.py @@ -474,6 +474,9 @@ class FastSlowBifurcation(object): if len(slow_vars) > 2: raise errors.ModelUseError("FastSlowBifurcation can only analyze the system with less " "than two-variable slow subsystem.") + for key in self.slow_vars: + self.model.variables.remove(key) + self.model.parameters.append(key) # check "fixed_vars" if fixed_vars is None: @@ -490,7 +493,7 @@ class FastSlowBifurcation(object): raise errors.ModelUseError('"pars_update" must be a dict the format of: ' '{"Par A": A_value, "Par B": B_value}') for key in pars_update.keys(): - if (key not in self.model.scopes) or (key not in self.model.parameters): + if (key not in self.model.scopes) and (key not in self.model.parameters): raise errors.ModelUseError(f'"{key}" is not a valid parameter in "{integrals}" model. ') self.pars_update = pars_update @@ -556,7 +559,7 @@ class _FastSlowTrajectory(object): else: self.slow_var_names = list(sorted(slow_vars.keys())) - def plot_trajectory(self, initials, duration, plot_duration=None, inputs=(), show=False): + def plot_trajectory(self, initials, duration, plot_duration=None, show=False): """Plot trajectories according to the settings. Parameters @@ -577,8 +580,6 @@ class _FastSlowTrajectory(object): The duration to plot. It can be a tuple with ``(start, end)``. It can also be a list of tuple ``[(start1, end1), (start2, end2)]`` to specify the plot duration for each initial value running. - inputs : tuple, list - The inputs to the model. Same with the ``inputs`` in ``NeuGroup.run()`` show : bool Whether show or not. """ @@ -629,7 +630,7 @@ class _FastSlowTrajectory(object): fixed_vars=self.fixed_vars, pars_update=self.pars_update, scope=self.model.scopes) - traj_group.run(duration=duration, report=False) + traj_group.run(duration=duration[init_i], report=False) # 5.3 legend legend = f'$traj_{init_i}$: ' diff --git a/brainpy/backend/__init__.py b/brainpy/backend/__init__.py index 94ee8c93..3492f92c 100644 --- a/brainpy/backend/__init__.py +++ b/brainpy/backend/__init__.py @@ -12,9 +12,9 @@ _net_runner = None _dt = 0.1 CLASS_KEYWORDS = ['self', 'cls'] -NEEDED_OPS = ['as_tensor', 'normal', 'reshape', 'shape', - 'exp', 'sum', 'zeros', 'ones', - 'eye', 'matmul', 'vstack', 'arange'] +NEEDED_OPS = ['normal', 'exp', 'matmul', 'sum', + 'as_tensor', 'zeros', 'ones', 'arange', + 'eye', 'vstack', 'reshape', 'shape', ] SUPPORTED_BACKEND = { 'numba', 'numba-parallel', 'numba-cuda', 'jax', # JIT framework 'numpy', 'pytorch', 'tensorflow', @@ -22,7 +22,7 @@ SUPPORTED_BACKEND = { SYSTEM_KEYWORDS = ['_dt', '_t', '_i'] -def set(backend, module_or_operations=None, node_runner=None, net_runner=None, dt=None): +def set(backend=None, module_or_operations=None, node_runner=None, net_runner=None, dt=None): """Basic backend setting function. Using this function, users can set the backend they prefer. For backend @@ -47,7 +47,7 @@ def set(backend, module_or_operations=None, node_runner=None, net_runner=None, d if dt is not None: set_dt(dt) - if _backend == backend: + if (backend is None) or (_backend == backend): return global_vars = globals() diff --git a/brainpy/backend/operators/bk_numpy.py b/brainpy/backend/operators/bk_numpy.py index 5457efce..47d6fcdb 100644 --- a/brainpy/backend/operators/bk_numpy.py +++ b/brainpy/backend/operators/bk_numpy.py @@ -21,6 +21,7 @@ __all__ = [ as_tensor = np.asarray normal = np.random.normal reshape = np.reshape +shape = np.shape exp = np.exp sum = np.sum zeros = np.zeros @@ -30,10 +31,3 @@ matmul = np.matmul vstack = np.vstack arange = np.arange - -def shape(x): - size = np.shape(x) - if len(size) == 0: - return (1,) - else: - return size diff --git a/brainpy/backend/runners/general_runner.py b/brainpy/backend/runners/general_runner.py index 7cfe15ad..0232482a 100644 --- a/brainpy/backend/runners/general_runner.py +++ b/brainpy/backend/runners/general_runner.py @@ -230,7 +230,7 @@ class GeneralNetRunner(runner.NetRunner): code_lines = ['def run_func(_t, _i, _dt):'] for obj in self.all_nodes.values(): f, codes = obj.build(inputs=formatted_inputs.get(obj.name, []), - input_is_formatted=True, + inputs_is_formatted=True, mon_length=run_length, return_code=True, show_code=show_code) diff --git a/brainpy/simulation/dynamic_system.py b/brainpy/simulation/dynamic_system.py index 53c07f12..26b63785 100644 --- a/brainpy/simulation/dynamic_system.py +++ b/brainpy/simulation/dynamic_system.py @@ -95,13 +95,15 @@ class DynamicSystem(object): else: raise errors.ModelDefError(f'Unknown setting of "target_backend": {self.target_backend}') - def build(self, inputs, input_is_formatted=False, return_code=True, mon_length=0, show_code=False): + def build(self, inputs, inputs_is_formatted=False, return_code=True, mon_length=0, show_code=False): """Build the object for running. Parameters ---------- inputs : list, tuple, optional The object inputs. + inputs_is_formatted : bool + Whether the "inputs" is formatted. return_code : bool Whether return the formatted codes. mon_length : int @@ -117,7 +119,7 @@ class DynamicSystem(object): raise errors.ModelDefError(f'The model {self.name} is target to run on {self._target_backend},' f'but currently the default backend of BrainPy is ' f'{backend.get_backend()}') - if not input_is_formatted: + if not inputs_is_formatted: inputs = utils.format_pop_level_inputs(inputs, self, mon_length) return self.runner.build(formatted_inputs=inputs, mon_length=mon_length, @@ -147,7 +149,7 @@ class DynamicSystem(object): # build run function # ------------------ - self.run_func = self.build(inputs, input_is_formatted=False, mon_length=run_length, return_code=False) + self.run_func = self.build(inputs, inputs_is_formatted=False, mon_length=run_length, return_code=False) # run the model # ------------- diff --git a/develop/benchmark/COBA/COBA_brainpy.py b/develop/benchmark/COBA/COBA_brainpy.py index 7c06b339..f7d00720 100644 --- a/develop/benchmark/COBA/COBA_brainpy.py +++ b/develop/benchmark/COBA/COBA_brainpy.py @@ -2,9 +2,7 @@ import time - import numpy as np - import brainpy as bp dt = 0.05 @@ -69,13 +67,13 @@ class LIF(bp.NeuGroup): self.input[i] = I -class EecSyn(bp.TwoEndConn): +class ExcSyn(bp.TwoEndConn): target_backend = ['numpy', 'numba'] def __init__(self, pre, post, conn, **kwargs): self.conn = conn(pre.size, post.size) self.pre2post = self.conn.requires('pre2post') - super(EecSyn, self).__init__(pre=pre, post=post, **kwargs) + super(ExcSyn, self).__init__(pre=pre, post=post, **kwargs) def update(self, _t): for pre_id, spike in enumerate(self.pre.spike): @@ -103,8 +101,8 @@ E_group = LIF(num_exc, monitors=['spike']) E_group.V = np.random.randn(num_exc) * 5. - 55. I_group = LIF(num_inh, monitors=['spike']) I_group.V = np.random.randn(num_inh) * 5. - 55. -E2E = EecSyn(pre=E_group, post=E_group, conn=bp.connect.FixedProb(0.02)) -E2I = EecSyn(pre=E_group, post=I_group, conn=bp.connect.FixedProb(0.02)) +E2E = ExcSyn(pre=E_group, post=E_group, conn=bp.connect.FixedProb(0.02)) +E2I = ExcSyn(pre=E_group, post=I_group, conn=bp.connect.FixedProb(0.02)) I2E = InhSyn(pre=I_group, post=E_group, conn=bp.connect.FixedProb(0.02)) I2I = InhSyn(pre=I_group, post=I_group, conn=bp.connect.FixedProb(0.02)) diff --git a/docs/apis/analysis.rst b/docs/apis/analysis.rst index 58cf6f36..8419bf74 100644 --- a/docs/apis/analysis.rst +++ b/docs/apis/analysis.rst @@ -39,10 +39,10 @@ brainpy.analysis .. autoclass:: PhasePlane :members: plot_fixed_point, plot_nullcline, plot_trajectory, plot_vector_field -.. autoclass:: PhasePlane1D +.. autoclass:: _PhasePlane1D :members: plot_fixed_point, plot_nullcline, plot_trajectory, plot_vector_field -.. autoclass:: PhasePlane2D +.. autoclass:: _PhasePlane2D :members: plot_fixed_point, plot_nullcline, plot_trajectory, plot_vector_field diff --git a/docs/index.rst b/docs/index.rst index ac1db41b..8dc2b53c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,17 @@ BrainPy documentation ===================== -``BrainPy`` is an integrative framework for computational neuroscience and brain-inspired computation. +``BrainPy`` is an integrative framework for computational neuroscience +and brain-inspired computation. It provides three core functions for +neurodyanmics modeling: + +- *General numerical solvers* for ODEs and SDEs (future will support DDEs and FDEs). +- *Neurodynamics simulation tools* for various brain objects, such like neurons, synapses + and networks (future will support soma and dendrites). +- *Neurodynamics analysis tools* for differential equations, including phase plane + analysis and bifurcation analysis (future will support continuation analysis and + sensitive analysis). + Comprehensive examples of BrainPy please see `BrainPy-Models `_. @@ -28,7 +38,6 @@ Comprehensive examples of BrainPy please see :maxdepth: 1 :caption: Advanced Tutorials - tutorials_advanced/repeat_mode tutorials_advanced/usage_of_inputs_module diff --git a/docs/tutorials/build_neurons.ipynb b/docs/tutorials/build_neurons.ipynb index ec1608fc..9cb9b2a1 100644 --- a/docs/tutorials/build_neurons.ipynb +++ b/docs/tutorials/build_neurons.ipynb @@ -1378,7 +1378,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.9" }, "toc": { "base_numbering": 1, @@ -1396,7 +1396,7 @@ "width": "243.07px" }, "toc_section_display": true, - "toc_window_display": false + "toc_window_display": true }, "varInspector": { "cols": { diff --git a/docs/tutorials/neurodynamics_analysis.ipynb b/docs/tutorials/neurodynamics_analysis.ipynb index 8c62a3c9..3eea70cb 100644 --- a/docs/tutorials/neurodynamics_analysis.ipynb +++ b/docs/tutorials/neurodynamics_analysis.ipynb @@ -13,7 +13,6 @@ "source": [ "**Contents**:\n", "\n", - "- [What's done in BrainPy](#What's-done-in-BrainPy)\n", "- [Phase Plane Analysis](#Phase-Plane-Analysis)\n", "- [Bifurcation Analysis](#Bifurcation-Analysis)\n", "- [Fast-Slow System Bifurcation](#Fast-Slow-System-Bifurcation)" @@ -32,7 +31,7 @@ "source": [ "As is known to us all, dynamics analysis is necessary in neurodynamics. This is because blind simulation of nonlinear systems is likely to produce few results or misleading results. For example, attractors and repellors can be easily obtained through simulation by time forward and backward, while saddles can be hard to find. \n", "\n", - "Currently, BrainPy supports neurodynamics analysis for low-dimensional neuron models. Specifically, BrainPy provides the following methods for neuron model analysis:\n", + "Currently, BrainPy supports neurodynamics analysis for low-dimensional dynamical systems. Specifically, BrainPy provides the following methods for dynamics analysis:\n", "\n", "1. phase plane analysis for one-dimensional and two-dimensional systems;\n", "2. codimension one and codimension two bifurcation analysis;\n", @@ -48,210 +47,19 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": {}, + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:10:39.678453Z", + "start_time": "2021-03-25T03:10:36.763061Z" + } + }, "outputs": [], "source": [ "import brainpy as bp\n", "import numpy as np" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## What's done in BrainPy" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The neuron model analysis is implemented in `brainpy.analysis`. It is used to analyze the phase portrait and bifurcation mechanism of 1D or 2D dynamical systems. It can also be used to analyze the high-dimensional system but with the fixation of other variables to preserve only one or two dynamical variables." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For a given two-dimensional system\n", - "\n", - "$$\n", - "{dx \\over dt} = f(x, t, y) \\\\\n", - "{dy \\over dt} = g(y, t, x),\n", - "$$\n", - "\n", - "once users code the system with the format of BrainPy, like" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "@bp.integrate\n", - "def int_x(x, t, y, p1):\n", - " f = 1.\n", - " return f\n", - "\n", - "@bp.integrate\n", - "def int_y(y, t, x, p2):\n", - " g = 1.\n", - " return g\n", - "\n", - "def update(ST, _t):\n", - " ST['x'] = int_x(ST['x'], _t, ST['y'], ST['p1'])\n", - " ST['y'] = int_y(ST['y'], _t, ST['x'], ST['p2'])\n", - "\n", - "example = bp.NeuType('example', steps=update, ST=bp.NeuState('x', 'y', 'p1', 'p2'))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "BrainPy will immediately know what integrators have you defined in the model. " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "example.integrators" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For each integrator, the user-defined variable name and its time variable name will be parsed out." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('x', 'y')" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "example.integrators[0].diff_eq.var_name, example.integrators[1].diff_eq.var_name" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('t', 't')" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "example.integrators[0].diff_eq.t_name, example.integrators[1].diff_eq.t_name" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Their dependence between each other will also be resolved." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(['x', 't', 'y', 'p1'], ['y', 't', 'x', 'p2'])" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "example.integrators[0].diff_eq.func_args, example.integrators[1].diff_eq.func_args" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "By using these informations and the function contents, it is easy to tranform the user-defined differential functions to expressions which are compatible with the computer algebra system (such as SymPy). " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Therefore, for a two-dimensional dynamical system, its phase plane and bifurcation analysis will follow the following steps:\n", - "\n", - "1. Use abstract syntax tree (AST) to parse the mathematical expressions defined by the user codes. And then convert them into expressions which are compatible with SymPy.\n", - "2. Try to solve $f(x, y)=0$ to get the dependece of $x = f_1(y)$ by using SymPy. \n", - " - If success, substitute $x = f_1(y)$ into $g(x, y) = 0$; then use the iterative method to find the function root $\\hat{y}$, and get $\\hat{x} = f_1(\\hat{y})$; go to step 5. \n", - " - If failed, try to solve $f(x, y) = 0$ to get the dependence of $y=f_2(x)$. \n", - " - If sucess, substitute $y = f_2(x)$ into $g(x, y) = 0$; then use the iterative method to find the function root $\\hat{x}$, and get $\\hat{y} = f_2(\\hat{x})$; go to step 5.\n", - " - If failed, go to step 3.\n", - "3. Try to solve $g(x, y)=0$ to get the dependece of $x = g_1(y)$. \n", - " - If success, substitute $x = g_1(y)$ into $f(x, y) = 0$; then use the iterative method to find the function root $\\hat{y}$, and get $\\hat{x} = g_1(\\hat{y})$; go to step 5. \n", - " - If failed, try to solve $g(x, y) = 0$ to get the dependence of $y=g_2(x)$. \n", - " - If sucess, substitute $y = g_2(x)$ into $f(x, y) = 0$; then use the iterative method to find the function root $\\hat{x}$, and get $\\hat{y} = g_2(\\hat{x})$; go to step 5.\n", - " - If failed, go to step 4.\n", - "4. Contruct a non-zero optimization function $h(x,y) = f^2(x,y) + g^2(x, y)$; use a gradient descent method to find all the minimum points in the whole space; For each minimum point $(\\bar{x}, \\bar{y})$, check whether the value of the optimization function $h(\\bar{x}, \\bar{y})$ is less than $10^{-8}$; If so, the point is the fixed point $h(\\hat{x}, \\hat{y})$ of the equation; go to step 5.\n", - "5. Make a small perturbation $\\delta$ at each fixed point $(\\hat{x}, \\hat{y})$, get the Jacobian matrix and judge its stability:\n", - "$$\n", - "\\left(\\begin{array}{ll}\n", - "{f(\\hat{x} + \\delta, \\hat{y}) - f(\\hat{x}, \\hat{y}) \\over \\delta} & \n", - "{f(\\hat{x}, \\hat{y}+ \\delta) - f(\\hat{x}, \\hat{y}) \\over \\delta} \\\\\n", - "{g(\\hat{x} + \\delta, \\hat{y}) - g(\\hat{x}, \\hat{y}) \\over \\delta} & \n", - "{g(\\hat{x}, \\hat{y}+ \\delta) - g(\\hat{x}, \\hat{y}) \\over \\delta}\n", - "\\end{array}\\right).\n", - "$$ \n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The bifurcation analysis is very similar to the phase plane analysis. What different is for each function $f(x, y)$, the parameter $p_1, p_2$ are inclued $f(x, y, p_1, p_2)$. " - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -289,33 +97,22 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:58:18.866966Z", + "start_time": "2021-03-24T11:58:18.854001Z" + } + }, "outputs": [], "source": [ - "bp.profile.set(dt=0.02, numerical_method='rk4') # will be useful in \"plot_trajectory()\"\n", - "\n", - "\n", - "a=0.7; b=0.8; tau=12.5; Vth=1.9\n", + "a=0.7; b=0.8; tau=12.5; Vth=1.9\n", "\n", - "@bp.integrate\n", - "def int_w(w, t, v):\n", - " return (v + a - b * w) / tau\n", - "\n", - "@bp.integrate\n", - "def int_v(v, t, w, Iext):\n", - " return v - v * v * v / 3 - w + Iext\n", - "\n", - "def update(ST, _t):\n", - " ST['w'] = int_w(ST['w'], _t, ST['v'])\n", - " v = int_v(ST['v'], _t, ST['w'], ST['input'])\n", - " ST['spike'] = np.logical_and(v >= Vth, ST['v'] < Vth)\n", - " ST['v'] = v\n", - " ST['input'] = 0.\n", - "\n", - "state = bp.types.NeuState('v', 'w', 'spike', 'input')\n", - "\n", - "FN = bp.NeuType(name='FN', ST=state, steps=update)" + "@bp.odeint\n", + "def int_fhn(V, w, t, Iext):\n", + " dw = (V + a - b * w) / tau\n", + " dV = V - V * V * V / 3 - w + Iext\n", + " return dV, dw" ] }, { @@ -324,7 +121,7 @@ "source": [ "Phase Plane Analysis is implemented in `brainpy.analysis.PhasePlane`. It receives the following parameters: \n", "\n", - "- ``model``: The neuron model to be analysis. It must be an instance of `brainpy.NeuType`.\n", + "- ``integrals``: The integral functions to be analysis. \n", "- ``target_vars``: The variables to be analuzed. It must a dictionary with the format of `{var: variable range}`.\n", "- ``fixed_vars``: The variables to be fixed (optional).\n", "- ``pars_update``: Parameters to update (optional)." @@ -351,8 +148,12 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:58:24.172655Z", + "start_time": "2021-03-24T11:58:18.870967Z" + }, "scrolled": false }, "outputs": [ @@ -360,18 +161,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "SymPy solve \"int_w(v, w) = 0\" to \"w = f(v, )\", success.\n", - "SymPy solve \"int_v(v, w) = 0\" to \"w = f(v, )\", success.\n", - "SymPy solve derivative of \"int_v(v, w)\" by \"v\", success.\n", - "SymPy solve derivative of \"int_v(v, w)\" by \"w\", success.\n", - "SymPy solve derivative of \"int_w(v, w)\" by \"v\", success.\n", - "SymPy solve derivative of \"int_w(v, w)\" by \"w\", success.\n", - "Fixed point #1 at v=-0.2729009589972752, w=0.5338738012534059 is a unstable-node.\n" + "plot nullcline ...\n", + "SymPy solve \"int_fhn(V, w) = 0\" to \"w = f(V, )\", success.\n", + "SymPy solve \"int_fhn(V, w) = 0\" to \"w = f(V, )\", success.\n", + "plot vector field ...\n", + "plot fixed point ...\n", + "SymPy solve derivative of \"int_fhn(V, w)\" by \"V\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w)\" by \"w\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w)\" by \"V\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w)\" by \"w\", success.\n", + "Fixed point #1 at V=-0.27290095899729705, w=0.5338738012533786 is a unstable node.\n", + "plot trajectory ...\n" ] }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -384,13 +189,13 @@ ], "source": [ "analyzer = bp.analysis.PhasePlane(\n", - " model=FN,\n", - " target_vars={'v': [-3, 3], 'w': [-3., 3.]},\n", - " fixed_vars={'Iext': 0.8, 'input': 0.8})\n", + " integrals=int_fhn,\n", + " target_vars={'V': [-3, 3], 'w': [-3., 3.]},\n", + " pars_update={'Iext': 0.8})\n", "analyzer.plot_nullcline()\n", "analyzer.plot_vector_field()\n", "analyzer.plot_fixed_point()\n", - "analyzer.plot_trajectory([{'v': -2.8, 'w': -1.8}],\n", + "analyzer.plot_trajectory([{'V': -2.8, 'w': -1.8}],\n", " duration=100.,\n", " show=True)" ] @@ -404,14 +209,18 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 4, "metadata": { - "scrolled": true + "ExecuteTime": { + "end_time": "2021-03-24T11:58:24.378721Z", + "start_time": "2021-03-24T11:58:24.172655Z" + }, + "scrolled": false }, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -423,11 +232,22 @@ } ], "source": [ - "group = bp.NeuGroup(FN, 1, monitors=['v', 'w'])\n", - "group.ST['v'] = -2.8\n", - "group.ST['w'] = -1.8\n", - "group.run(100., inputs=('ST.input', 0.8)) \n", - "bp.visualize.line_plot(group.mon.ts, group.mon.v, legend='v', ) \n", + "class FHN(bp.NeuGroup):\n", + " target_backend = 'numpy'\n", + " \n", + " def __init__(self, num, **kwargs):\n", + " self.V = np.ones(num) * -2.8\n", + " self.w = np.ones(num) * -1.8\n", + " self.Iext = np.zeros(num) \n", + " super(FHN, self).__init__(size=num, **kwargs)\n", + " \n", + " def update(self, _t):\n", + " self.V, self.w = int_fhn(self.V, self.w, _t, self.Iext)\n", + "\n", + " \n", + "group = FHN(1, monitors=['V', 'w'])\n", + "group.run(100., inputs=('Iext', 0.8, '=')) \n", + "bp.visualize.line_plot(group.mon.ts, group.mon.V, legend='v', ) \n", "bp.visualize.line_plot(group.mon.ts, group.mon.w, legend='w', show=True)" ] }, @@ -451,7 +271,7 @@ "source": [ "Bifurcation analysis is implemented within `brainpy.analysis.Bifurcation`. Which support codimension-1 and codimension-2 bifurcation analysis. Specifically, it receives the following parameter settings: \n", "\n", - "- ``model``: The neuron model to be analyed. Must be an instance of `brainpy.NeuType`.\n", + "- ``integrals``: The integral functions to be analysis.\n", "- ``target_pars``: The target parameters. Must be a dictionary with the format of `{par: parameter range}`. \n", "- ``target_vars``: The target variables. Must be a dictionary with the format of `{var: variable range}`. \n", "- ``fixed_vars``: The fixed variables.\n", @@ -474,23 +294,29 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:58:26.557712Z", + "start_time": "2021-03-24T11:58:24.381727Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "SymPy solve \"int_w(v, w, Iext) = 0\" to \"w = f(v, Iext)\", success.\n", - "SymPy solve derivative of \"int_v(v, w, Iext)\" by \"v\", success.\n", - "SymPy solve derivative of \"int_v(v, w, Iext)\" by \"w\", success.\n", - "SymPy solve derivative of \"int_w(v, w, Iext)\" by \"v\", success.\n", - "SymPy solve derivative of \"int_w(v, w, Iext)\" by \"w\", success.\n" + "plot bifurcation ...\n", + "SymPy solve \"int_fhn(V, w, Iext) = 0\" to \"w = f(V, Iext)\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, Iext)\" by \"V\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, Iext)\" by \"w\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, Iext)\" by \"V\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, Iext)\" by \"w\", success.\n" ] }, { "data": { - "image/png": "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a3ZAVFm2bBmTJ08mIyOD7OxsnnzyST744APOPfdcunbtyrx58ygoKGDQoEH07duXUaNG1Xr9vn37GDFiBJWVlcycObPe/l966SVuvPFGHnjgAQ4cOMAll1zCkCFDItZ00003MXHiRHJzc8nKyuL555/nkEMO4Xvf+x6rV68mLy+P7OxsbrjhBm655Rbuuecerr/+en7xi18wYsSI6v088sgjzJs3j8zMTI4//njOPffcqPzMRCSgtHRmxDGJWIYEgIXrfkhmhYWFXvfGRStWrGDgwIEJqkgOhn5nIoErrosW/hv1J4UGxiSi1d1kZgvdvTDUc2mz1pOISDIqLp5MqJCI5ZhEXQoKEZEW6uMFF/G/e9fV2x7rMYm6FBQiIi3Q4sXX8O23n9Tb3qXzOAYNejiutSgoRERamOLiSXy947162zt2OCXuIQEKChGRFiXcNNhWrbpQUPB8/AtCQSEi0mKUlS0KOw02L/fxOFfzTwqKJLJz506eeOKJBtutW7eOwYMHh3xu9OjR1J06HEvvvvsu559/ftzeTySZLV32g5DbBxz3QFTvL9FUCook0tigEJHks3jxNezf/2W97dG8VuJgKSgiiOatBet+yp82bRr33nsvo0eP5o477mD48OEce+yxvPdeYACruLiY4cOHk5+fT15eHp999hl33nknn3/+Ofn5+UyePJldu3YxZswYhg4dSm5uLq+//s9+zfLycq6++mry8vK4+OKLQ9734q233uLEE09k6NChjB8/nl27dtVr8+677zJ69GguvvhiBgwYwOWXX169RtTbb79NQUEBubm5XHfddezbtw8IrJQ7YMAATj75ZGbNmlW9r927d3PddddxwgknUFBQUKtekXQWbvC6S+dxCQ8JUFCEVVa2iEWLr+TzNQ+zaPGVMbkPbZXy8nI+/vhjHnnkEe677z4Apk+fzm233caSJUsoKiqiR48eTJkyhWOOOYYlS5bw0EMPkZOTw2uvvcaiRYuYN28eP/7xj6v/iK9atYoJEyawdOlS2rZtW+9MZNu2bTzwwAPMnTuXRYsWUVhYyMMPh55NsXjxYh555BGWL1/OmjVr+Mc//sHevXu55ppreOWVV1i2bBnl5eU8+eST7N27lxtuuIE//vGPvPfee3z55T8/IT344IOcfvrpLFiwgHnz5jF58mR2794do5+qSHIIN3jdps2Q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\n", + "image/png": "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mamwoSSt68ndq83seuAVIBv4vbuXmdk1wf2PA3cChwOPBm/gqg43Ki8i0hpVVs4IuY5TIp953VJJvY/C3ee4+2B3P9rd7wtuezwYErj71e7exd0Lg18ZHxLJWGb8gw8+oLCwkKKiIj766KOo3ycisObj3zTa1rXLiQl95nVL6c7sFJOMZcZfeOEFxo8fz7Jlyzj66KMTemyRVFZWdjN79245YFt2dm7SnlR3sBQUESyu2snD6zaxuGpniz8rVZcZnzNnDg899BDTpk3j1FNPBWDKlCnk5+eTn5/PQw89VNf2mWeeoaCggOHDh3PppZcCMHHiRJ577rm6Nh07dgRg48aNnHTSSRQWFpKfn193ziLpItzd14MGhh7Ubs20KGAYi6t2cn5pOftqnJws47nCgRTndojb8WqX8Z4zZw533303c+fOrVtm/OKLL2bv3r3s37+f+++/nxUrVlBaWlr3vlmzZtG5c2e2bt3K6NGjOeecc4DAMuNPPPEEY8aM4corr+TRRx89YPXY+st+d+jQgcmTJzNlyhTuvPPfNwSNGzeOa665ho4dO3LLLbewZMkSnnzySd555x3cnVGjRnHyySfTtm1b7rvvPv71r3/RrVs3Pvvss4jn+6c//YkzzjiDO+64g/3794cMMZFUFW4qbIcOx6bMuER9CoowFnyxg301zn6AGmfBFzviGhStfZnxWm+++SbnnnsuHTp0qKv7jTfewMw4//zz6datGwBdu3aN+DkjR47kyiuvZN++fXz729+msLCwiX8hkdRRXv6rkNuPHfKLBFcSG+p6CuOEwzqSk2VkAzlZxgmHdWzR5x3sMuOzZ8/m0EMP5YwzzuDVVxuvA1N/mfHS0lJ69uzZ7GXGS0tLKS0t5f333+eJJ56IeB7h1gZz95BLi9c/b3dn7969QGDl2ddff50+ffpw6aWX8swzz0Q8rkiqqKpayhdVixptT5WpsKEoKMIozu3Ac4UDuW1A75h0O6XLMuMnnXQSL7zwArt27WLnzp3MmjWLE088kbFjx/Lss8+ybVvgnsjarqe8vLy6J9y9+OKL7Nu3D4B169bRo0cPvve973HVVVexdGl8nyAokiih1nJKhbuvI1HXUwTFuR1i1t1Uf5nx/v37R73M+B//+EdycnLo1asXd955J127dq1bZvyss87itttu41vf+hbFxcUUFhaGXGb8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\n", "text/plain": [ "
" ] @@ -502,7 +328,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZAAAAEGCAYAAABLgMOSAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAApwklEQVR4nO3deXyU5d3v8c8vIYgGRHYoiAEFoYSQYAgqRaHUtVaq1aPWDTeqPtrt6MGn9rGLeqrVY609LuVBpfpYim1FOVatxeUBaxUIRCQINCBqAsoiRhZZkvzOHzNJJ8lMMpnMksl8369XXmTuZeZ3C+ab+7qu+7rM3REREWmrrFQXICIi6UkBIiIiMVGAiIhITBQgIiISEwWIiIjEpEuqC0imvn37el5eXqrLEBFJK6WlpdvdvV/T7RkVIHl5eSxfvjzVZYiIpBUz+yDcdjVhiYhITBQgIiISEwWIiIjEJKP6QMI5ePAglZWV7Nu3L9WlSBS6devGkCFDyMnJSXUpIhkv4wOksrKSHj16kJeXh5mluhxpgbuzY8cOKisrGTZsWKrLEcl4Gd+EtW/fPvr06aPwSANmRp8+fXS3KNJBZHyAAAqPNKK/K5G2q65ewaZND1NdvSKu75vxTVgiIp1VdfUK3lt7G3v2vAcYWVmHML7oSXr2HB+X99cdSAd1//33s3fv3laPy8vLY/v27c22//SnP+Xee++N+vP279/P1772NQoLC5k/f36bahWRjqeqah7LS88PhgeAU1e3n507347bZyhAOqhoAyReVq5cycGDBykrK+OCCy5I2ueKSPxVVc1j7bofh93Xq9fEuH2OAiQGH2+spvSlTXy8sbrd77Vnzx6+/vWvM27cOPLz85k/fz4PPPAAmzdvZurUqUydOhWA6667juLiYsaMGcNPfvKTRu9xzz33UFJSQklJCRUVFc0+Y8OGDZx++ukcd9xxTJ48mbVr1zbav3XrVi655BLKysooLCxkw4YNvPLKKxQVFTF27FiuvPJK9u/fD8CyZcs48cQTGTduHCUlJezatYu5c+dyww03NLzfWWedxeuvv05tbS0zZswgPz+fsWPH8qtf/ard/71EpGXV1SsihsfAAWfHrfkK1AfSZh9vrOa5X62ktqaO7C5ZTP9BEQOH94z5/V566SW+9KUv8Ze//AWA6upqevbsyX333cdrr71G3759Abjzzjvp3bs3tbW1TJs2jVWrVlFQUADA4YcfztKlS3niiSf4/ve/z/PPP9/oM2bOnMkjjzzCiBEjePvtt7n++ut59dVXG/b379+fOXPmcO+99/L888+zb98+pkyZwiuvvMLIkSO57LLLePjhh7n++uu54IILmD9/PhMmTODzzz/n0EMPjXhtZWVlVFVVsXr1agA+++yzmP87iUjrqqtXsLLsirD7eveazJgx98X183QH0kZV63dSW1OHO9TW1lG1fme73m/s2LEsWrSIWbNmsWTJEnr2DB9GTz/9NOPHj6eoqIjy8nLWrFnTsO+iiy5q+PMf//hHo/N2797Nm2++yfnnn09hYSHf+c532LJlS4s1rVu3jmHDhjFy5EgALr/8chYvXsy6desYNGgQEyZMAALB1aVL5N9Bhg8fzsaNG7nxxht56aWXOPzww1v/DyIiManv86it3d1s38AB0ykqmhv3z1SAtNHgkb3I7pKFZUF2dhaDR/Zq1/uNHDmS0tJSxo4dy7//+7/z85//vNkx77//Pvfeey+vvPIKq1at4utf/3qjZyFCh7Y2HeZaV1fHEUccQVlZWcPXe++9R0vcPeL2cMNou3TpQl1dXcPr+tp69erFO++8w5QpU3jwwQe5+uqrW/xcEYlNS81WibjzqKcAaaOBw3sy/QdFTDx7eLubrwA2b97MYYcdxiWXXMJNN93EihWBcdo9evRg165dAHz++efk5ubSs2dPPvnkE1588cVG71E/amr+/PmccMIJjfYdfvjhDBs2jD/+8Y9AIATeeeedFmsaNWoUmzZtauhPefLJJzn55JMZNWoUmzdvZtmyZQDs2rWLmpoa8vLyKCsro66ujo8++oilS5cCsH37durq6vjWt77F7bff3nBtIhJfq969Mez2Hj3GJeTOo576QGIwcHjPdgdHvXfffZebb76ZrKwscnJyePjhh4FAv8UZZ5zBoEGDeO211ygqKmLMmDEMHz6cSZMmNXqP/fv3M3HiROrq6pg3b16zz3jqqae47rrruOOOOzh48CAXXngh48aNi1hTt27dePzxxzn//POpqalhwoQJXHvttXTt2pX58+dz44038sUXX3DooYeyaNEiJk2axLBhwxg7diz5+fmMHx/opKuqquKKK65ouDv5xS9+EZf/ZiLyL0uXncuBAx8329671+SEhgeARWqu6IyK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\n", "text/plain": [ "
" ] @@ -515,9 +341,9 @@ ], "source": [ "analyzer = bp.analysis.Bifurcation(\n", - " model=FN,\n", + " integrals=int_fhn,\n", " target_pars={'Iext': [0., 1.]},\n", - " target_vars={'v': [-3, 3], 'w': [-3., 3.]},\n", + " target_vars={'V': [-3, 3], 'w': [-3., 3.]},\n", " numerical_resolution=0.001,\n", ")\n", "analyzer.plot_bifurcation(show=True)" @@ -539,23 +365,29 @@ }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, + "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:58:29.634679Z", + "start_time": "2021-03-24T11:58:26.561712Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "SymPy solve \"int_w(v, w, a, Iext) = 0\" to \"w = f(v, a,Iext)\", success.\n", - "SymPy solve derivative of \"int_v(v, w, a, Iext)\" by \"v\", success.\n", - "SymPy solve derivative of \"int_v(v, w, a, Iext)\" by \"w\", success.\n", - "SymPy solve derivative of \"int_w(v, w, a, Iext)\" by \"v\", success.\n", - "SymPy solve derivative of \"int_w(v, w, a, Iext)\" by \"w\", success.\n" + "plot bifurcation ...\n", + "SymPy solve \"int_fhn(V, w, a, Iext) = 0\" to \"w = f(V, a,Iext)\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, a, Iext)\" by \"V\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, a, Iext)\" by \"w\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, a, Iext)\" by \"V\", success.\n", + "SymPy solve derivative of \"int_fhn(V, w, a, Iext)\" by \"w\", success.\n" ] }, { "data": { - "image/png": "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OfXUU9mwYQNr1qzhrrvuwmq19uieDTYc1eQQafq2nkDXdTZt2oTb7WbBggUkJSWhaQ4qKp5F130p37Ru2+kPuN2tlB56CildXonGRWnpn3A6atB1J6raiqY1YTYbSE5OJjk5GaPRiKZp2O12rFYrTqezA1l0RxoXXHAB4Dn1uqysDIBFixbxm9/8hkcffZSysjKSkpI61ZNSct999zFnzhy+853vUFlZSW1tLQCjR4/2H6l35ZVX8t///rdD3W+++YY9e/Zw0kknMW/ePP71r39RXl4ecrwLFixg1KhRKIrC7NmzKSsro7i4mHHjxjF16lSEEFxzzTX85z//YdeuXYwbN44JEyYgpeSqq67yt/PJJ5/wm9/8htmzZ3PyySfjcDj8fQ51HLXWikjTt/UEzc3N2Gw2pk6dSl5env/zurr3sNkOed8NhBIyNJyuOoQwImVgBigjTlctJlMGum5D1x1I6cZozMBgSMVoNPpzGPgC0HwSmE9fExjuDB1DnhMSPAFhgadeX3HFFSxcuJBVq1Zxzjnn8H//939MmDChw1hXrFhBQ0MD69evx2QyMXHiRH+7odzYAyGl5PTTT+9wOC7AunXruOmmm1AUhQcffJC0tDT/+ALH2FXC30CnLJ+ewncY8sqVK/2EcjThqJMcepK+LZq2fduI5OTkDsSgaTYqq170mhB1BlPMWoI5HymDTtmWKgnmPDwWDc3riu3E5arD7W7oUF4IgdFoJDExkZSUFJKSklAUhdzcXGprazl8+DDt7e3+jFDhcPDgQSZMmMBtt93Geeedx44dOzqcyg2ebFJ5eXmYTCbWrFnjlzoAysvL/UlhVqxY0elg3kWLFrF27Vp/yjmbzUZxcTHHH388X331FZs2beK8884LO75p06ZRVlbmr//SSy9x0kknMW3aNA4dOsTBgwcRQvhDzaWUnHbaafzxj3/06yw2bdp01Cg4jypyCM67EA0pdPdj+qwRvm1EsAm0tu4DXK4q77vBIzUAmEwZjB93K0KYUZRkhDAzftytmEwZ3hIei4ZnO+RG02y4XHXoeujrCCSL++67j9NPP52LLrqISZMm+ZWaoZSbr732GrNmzWLevHns3buXa665hpycHJYsWcKsWbO4++67ueyyy9i0aRPHH388r7zySocEK9OnT+df//oXc+bMoampiZtuuqlD+3l5eTz//PNc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X37U8JHDT37yEzl27Fj/czFx4kT53HPPSSk9ZDxixAh54YUX+usdTeQwKE2ZfYFIbeA+vPfee/zhD3/gs88+6+eR9j10XWfjxo28/vrrfPLJJ0ycONGfkyItLS2iNnxJaWbOnOnXU9hsNlJSUmhqauLgwYO89957vP322706ZnCg4FNI/uQnP2HKlCn88Ic/7FRm586dnH322YwdO5b//Oc/KIrC3//+dzZu3NghtigCDEpT5qDUOfQFAs2jZrPZbx4Nh1deeYUrrriiH0fYf1AUhYULF/LYY4+xZcsW7rvvPoqLizn77LO57LLLeOmll2hpaQlb35eFu6ioiPT0dIYNG0ZRURELFixACMHDDz/M//7v/zJp0iS2b9/efxfWB/jOd77D3/72N7/lorKy0n8i2nXXXcfLL7/M9OnT+f3vfw9AWloa7e3tAznkmOGYIYdQ5tHKysqQZW02Gx999BEXXXRRfw1vwKAoCnPmzOGRRx5h06ZN/PrXv6ayspLzzz+fCy64gH/84x80Njb6y1ssFnbs2EFRUVGnFHkGg4Hdu3djNBo5cOAAS5cuHfInTZ955plceeWVLF68mMLCQi6++GLa29t55JFHOPHEEznxxBP5/e9/z3PPPceePXs45ZRT2L17d1whOZTw2muvye9///v+9//85z/lrbfeGrLsq6++Ks8999z+GtqghK7rcu/evfKhhx6Sxx9/vDz11FPl/fffL0866SRZXV0dUhfx4osvyhNPPFG2tLTEdCzd6YpefPFFWVhYKAsLC+XixYvl1q1bY9p/P2DA9Quh/o4Zcli7dq0888wz/e8feeQR+cgjj4Qsu3z5cvnSSy/119AGPXRdl6tXr5bDhw+XJ554ojzppJPk448/LktKSvxKyBUrVsjFixfLpqammPatqqqcMGGCPHDggN9ytWvXrg5l/vvf//r7XbVqlVy4cGFMx9APGHAiCPU3KJ2g+gKBNvCRI0fy6quv8vLLL3cq19rayhdffMGLL744AKMcnBBCsHbtWj766CMKCwupqKhg5cqVXH/99X5F744dO/j444+jzp3ZHSJxpQ8M4lu0aBEVFRUxHcOximNG52A0Gnnqqaf4zne+w/Tp07n00kuZOXMmzzzzjD+CFDzOLZGknD/WcN9991FUVOTPSfH//t//44svvuD111/HYDDwyiuvhA0H7w2i0RUBPP/885x99tkxH8cxiW5EizgiQHd74paWFnnuuefKoqIiOWPGDPm3v/1tAEY5NBGNruizzz6T06ZNkw0NDf01vFhhwLcQof6OGcmhr6BpGj/60Y/48MMP2b17N6+88gq7d+/uUObPf/4zM2bMYNu2bXz++ef85Cc/idid+VhHJK70ANu3b+eGG27gnXfe6RMJ5lhEnBx6iUj8J4QQtLe3I6XEYrGQnZ3d5WnecRxBJPES5eXlXHjhhfzrX/86ZgLv+gPxJ7SXiCS8/NZbb+X8889nxIgRtLe3s2LFij4JWT4aEagr8mUa8+mKwJNp7Je//CWNjY3ccsst/jobN24cyGEfFYiTQy8hZffh5R9//DGzZ8/ms88+82dzOvHEE7s9Pi8OD84555xOIdC+A2cAnnvuOf/RCHHEDvHlq5eIZE/8wgsvcOGFFyKEYNKkSYwfP37Iew7GcfQjTg69RCR74jFjxvDpp58CUFtby759+/x2+zjiGKyIk0MvEYn/xH333cfatWspLCzktNNO47e//W1EZ3Z89NFHTJ06lUmTJvGb3/ym0/fNzc1ccMEFFBUVsXDhQnbu3Bnz64vj2MUxE7I91BBJiPldd91FamoqDzzwAHv37uVHP/qRX0KJY0ghHrIdR+SIxES6e/duTjvtNACmTZvGoUOHqK2tHYjhRoXuJCIpJbfffjuTJk2iqKiIzZs3D8Ao44iTwyBFJG7Ds2bN4s033wQ8ZFJWVjbo4woicRr78MMPKSkpoaSkhGeffZabb755gEZ7bCNODoMUkZhI77nnHpqbm5k9ezZPPvlkh4NqBysikYjeeecdrr32WoQQLFq0iJaWFqqrqwdoxMcuBveT1E9Yvnw5hw8fxuFw8OMf/5gf/OAHAz2kiA8ofuGFFwAPmYwfP57x48f36zijRaQHz4SSmoYPH95v44yDbgOvjok/INv7PwnYCeQMgjEZgYPAeMAMbANmBpXJBMze1zcC/xzocUdwXZcAzwW8vwZ4MqjMB8C3At5/Cswb6LEfa39xycGD24UQF3hfjwYmA41dlO9zSClVIcStwMeAAfiblHKXEOIm7/fPANOBfwohNGA38P0BG3DkqMBzj30YBVT1oEwcfY2BZqeB/gNOBr4Ckr3vPwdOHuhx9cN1/w2oA3aG+V4AfwL2A9uBuTHqNxKJaCnwoXcMi4D1A32/jsW/uEISMoBmKaVNCDENz8N4LODvwFldfH82HglqMvAD4OlYdCqlVAGfRLQHeE16JSKfVASswkMg+4G/ArfEou84okN3TlBHPYQQCcDbwEhgH5AHPCil/HwAh9UvEEKMA96XUh4X4rv/Az6XUr7ifb8Pj0QVNxscIzjmdQ5SSieeVTKOjhgJHA54X+H9LE4Oxwji24o4wiGUS++xLWYeY4iTQxzhELcYHOOIk0Mc4fAucK3wYBHQGtc3HFs45nUOxyqEEK/gMePmCiEqgAcAE/h9KFYB5+CxGNiA6wZmpHEMFI55a0UcccQRGvFtRRxxxBEScXKII444QiJODnHEEUdIxMkhjjjiCIk4OcQRRxwhESeHOOKIIyTi5BBHHHGExP8Hhps4eAVGMmAAAAAASUVORK5CYII=\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -567,7 +399,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAQgAAADyCAYAAACxiFs0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAB8KklEQVR4nO2dd3wb9f3/n3ca3ju2s/ckiTMdEiiUXUIgCWWU/S0UWnb5fQsttGWUAm2BUkrTQvstZe+wadgECmVkEGcndhJvO95DW7q7z+8P6WRJlmzJlkcSvR4PPyzd/NzpPq9777ckhCCBBBJIIBzkoR5AAgkkMHyRIIgEEkggIhIEkUACCUREgiASSCCBiEgQRAIJJBARxl7WJ1wcCSQw8JCGegCRkJAgEkgggYhIEEQCCSQQEQmCSCCBBCIiQRAJJJBARPRmpExgmMLj8VBTU4PT6RzqoSQQJZKTkxk7diwmk2mohxI1pF5yMRJejGGK8vJyMjIyyMvLQ5KGrRE8AR+EELS0tGCxWJg0aVLo6mH7AyZUjEMUTqczQQ6HECRJIi8v75CT+BIEcQgjQQ6HFg7F3ytBEAkkkEBEJAgigbji4Ycfxm6397rdxIkTaW5u7rb8rrvu4sEHHxyIoUVEenr6oJ7vUEKCIBKIK6IliAQODSQI4giC0tqKY/t2lNbWuBzPZrOxYsUK5s2bx5w5c/jNb35DXV0dJ554IieeeCIA11xzDYsXL2b27NnceeedQfs/8MADLFmyhCVLlrBv375ux9+/fz+nn346ixYt4rjjjmPPnj1hx5Gens6vfvUr5s2bx9KlS2loaACgsrKSk08+maKiIk4++WSqqqoArwdo2bJlFBcXc/vtt3cbU3FxMUVFRd3Ge0RCCNHTXwLDFLt27Ypp+/a33xa7580XexYtFrvnzRft77zT7zGsXbtWXHnllV3naG8XEyZMEE1NTf5lLS0tQgghFEUR3/3ud8XWrVuFEEJMmDBB3HPPPUIIIZ566imxYsUKIYQQd955p3jggQeEEEKcdNJJorS0VAghxNdffy1OPPHEsOMAxFtvvSWEEOKWW24Rv/3tb4UQQpx55pniySefFEII8fjjj4tVq1YJIYQ466yzxFNPPSWEEGLNmjUiLS1NCCHE+++/L6666iqhaZpQVVWsWLFCfPbZZ/25Rd0Q4XfrbR4O2V9CgjgCoLS2Uv/r2xFOJ5rVinA6qf/Vr/stScydO5ePPvqIX/ziF3z++edkZWV12+bll19m4cKFLFiwgJ07d7Jr1y7/ugsvvND//6uvvgraz2q18uWXX3Leeecxf/58fvKTn1BfXx92HGazmTPPPBOARYsWUVFRAcBXX33FRRddBMCll17KF198AcB///tf/7kvvfRS/3E++OADPvjgAxYsWMDChQvZs2cPZWVlfbk1hw0SkZRHADy1tUhGY1DUm2Q04qmtxZib2+fjTp8+nc2bN7Nu3Tpuu+02TjvttKD15eXlPPjgg2zcuJGcnBx++MMfBsUBBLr9Ql2AmqaRnZ1NSUlJ0HJVVVm0aBEAK1eu5O6778ZkMvn3NxgMKIoSdrw9nQ+80vRtt93GT37ykyiu/shAQoIYAgghUFUVMUgVxU1jxiBCJo1QFExjxvTruHV1daSmpnLJJZdw88038+2335KRkYHFYgGgs7OTtLQ0srKyaGho4N133w3a/6WXXvL/X7ZsWdC6zMxMJk2axCuvvOIdrxBs3boVg8FASUkJJSUl3H333T2O75hjjuHFF18E4LnnnuM73/kOAMcee2zQch3f+973+Ne//oXVagWgtraWxsbGPt2bwwUJCWKQIYTA7XbjdDoRQmAwGDCZTBiNRgwGw4AE0xhzcxl17z3U/+rXXklCURh17z39kh4Atm/fzi233IIsy5hMJh599FG++uorli9fzqhRo1i/fj0LFixg9uzZTJ48mWOPPTZof5fLxdFHH42mabzwwgvdjv/cc89xzTXXcM899+DxeLjggguYN29e1ON75JFHuOKKK3jggQfIz8/niSeeAODPf/4zF110EX/+858555xz/Nufdtpp7N69209W6enpPPvssxQUFPTl9hwWSORiDCI0TcPtdiOEQFGUIGOQjmgJY/fu3cyaNSum8yutrXhqazGNGdNvckigb4jwuw3bEMuEBDEIEELgcrnQNA1Zlv2TXpKkIAIQQqBpGlu2bGHixImkpKTEVcIw5uYmiCGBmJCwQQwwdKnhm2++QdM0/wQPJ7lJkoQse38SWZaRZRlN03A4HFitVjo6OrBYLH71ZLBsGAkcuUhIEAME3RDp8XiA7tJCNND30UlDlzAcDof/+Po2gVJJAgnECwmCGADoNgZFUYImebRvfEmSIkoYgYShbxe4bYIwEognEgQR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\n", + "image/png": "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Ir4f+r777hNTp04V06ZNE3/605+6POf7778vJk2aJMaPHy/uvffeDvvvu+8+MWvWLDFr1iwxbdo0IcuyaGxs7LVrGkgoiiK+/PJL8ctf/lJMnz5dnH322eK5554TdXV1CZNFS0uLKCsrE59//rl4++23xbp168SBAweEw+EII4eHHnpILF26VHg8noG+/IShE8QNN9wgRo8eHXouxo8fL/75z38KIQKEXFxcLM4444zQcYcaQQxKM2dfIF4buY533nmHP/3pT3zyySf9PNK+h6ZprF27lldffZV//etfjB8/PpTTIjPO9PR6Yptp06aF9BYul4v09HSamprYs2cP77zzDm+++Wavhaz3JzIyMnA4HNxwww1MmjSJn//85x3abN68mZNPPpnRo0fzxRdfIMsyTz31FGvXrg2LRYoDg9bMOSh1EH0Bo+nUarWGTKex8OKLL/KTn/ykH0fYf5BlmQULFnD//fezfv16br31Vnbu3MnJJ5/Mueeey/PPP09LS0vM4/Xs3jNnziQrK4uhQ4cyc+ZMSkpKkCSJu+++m1tuuYUJEyawcePG/ruwPsCPfvQjnnjiCRwOBxCwsOmV1y677DJeeOEFpk6dyoMPPghAZmYmdrt9IIfcqzhsCCIe06kOl8vFBx98wJlnntlfwxswyLLMnDlzuOeee1i3bh333nsvVVVVLFu2jNNPP52nn36axsbGUHuHw8GmTZuYOXNmh3R7JpOJrVu3Yjab2b17N0uXLmX79u39fUm9ipNOOonzzz+fRYsWMWPGDM466yzsdjv33HMPixcvZvHixTz44IP885//ZNu2bRx77LFs3bo1qaQ82PDKK6+In/70p6HvzzzzjLjuuuuitn3ppZfEqaee2l9DG5TQNE1s375d3HXXXeLII48Uxx13nLjtttvEMcccI2pqaqLqJp577jmxePFi0dLS0qtj6Up39Nxzz4kZM2aIGTNmiEWLFonS0tJe7b8fMOC6hlh/hw1BfP311+Kkk04Kfb/nnnvEPffcE7XtihUrxPPPP99fQxv00DRNfPTRR2LYsGFi8eLF4phjjhEPPPCAKCsrCykmX375ZbFo0SLR1NTUq30riiLGjRsndu/eHbJobdmyJazNf/7zn1C/q1atEgsWLOjVMfQDBpwIYv0NSkepvoDRRj58+HBeeuklXnjhhQ7tWltb+fzzz3nuuecGYJSDE5Ik8fXXX/PBBx8wY8YMKisrWblyJZdffnlI+btp0yY+/PDDhHNxdoV43O6NgX8LFy6ksrKyV8dwOOOw0UGYzWYeeeQRfvSjHzF16lTOOeccpk2bxt///vdQ5CkEHGDiSWd/uOHWW29l5syZoZwW//Vf/8Xnn3/Oq6++islk4sUXX+yTUPJEdEcAjz/+OCeffHKvj+OwRRciRhJxoKs1cktLizj11FPFzJkzxRFHHCGeeOKJARjlwYlEdEeffPKJmDJlimhoaOiv4fUWBnwpEevvsJEg+gqqqnLttdfy/vvvs3XrVl588UW2bt0a1uavf/0rRxxxBBs2bOCzzz7jhhtu6FYw1OGIeNzuATZu3MgVV1zBW2+91a9JcQ51JAmih4jHv0KSJOx2O0IIHA4HeXl5vZ6D4VBFPPEV+/fv54wzzuDZZ589bIL1+gvJp7SHiCcHwXXXXceyZcsoLi7Gbrfz8ssv90m486EIo+5Iz1im644gkLHsd7/7HY2NjVxzzTWhY9auXTuQwz5kkCSIHkKIrkPTP/zwQ2bPns0nn3wSygq1ePHiLkv1JRHAKaec0iF8Wi9qA/DPf/4zVHYhid5F8jXWQ8SzRn7yySc544wzkCSJCRMmMHbs2IPewzCJwwNJgugh4lkjjxo1io8//hiA2tpaduzYEbLrJ5HEYEaSIHqIePwrbr31Vr7++mtmzJjB8ccfzx/+8Ie4aoJ88MEHTJ48mQkTJvD73/++w/7m5mZOP/10Zs6cyYIFC9i8eXOvX18ShzcOm3Dvgw3xhKf/+te/JiMjg9tvv53t27dz7bXXhiSVJA4qJMO9k0gM8ZhPt27dyvHHHw/AlClT2LdvH7W1tQMx3ITQlWQkhOAXv/gFEyZMYObMmXz//fcDMMokIEkQgxbxuBjPmjWL119/HQgQSnl5+aCPQ4jHsez999+nrKyMsrIyHnvsMa6++uoBGm0SSYIYpIjHfHrTTTfR3NzM7Nmz+ctf/hJWHHewIh7J6K233uLiiy9GkiQWLlxIS0sLNTU1AzTiwxuD+2nqJ6xYsYKKigo8Hg+//OUvufLKKwd6SHEXRX7yySeBAKGMHTuWsWPH9us4E0W8xW2iSU/Dhg3rt3EmEcRAB4MMhj8gL/g/FdgM5A+CMZmBPcBYwApsAKZFtMkBrMHPPwOeGehxx3FdZwP/NHy/CPhLRJv3gB8Yvn8MzBvosR+Of0kJIoBfSJJ0evDzSGAi0NhJ+z6HEEKRJOk64EPABDwhhNgiSdJVwf1/B6YCz0iSpAJbgZ8O2IDjRyWBe6xjBFDdjTZJ9AcGmqEG+g9YAnwFpAW/fwYsGehx9cN1PwHUAZtj7JeAh4FdwEZgbi/1G49ktBR4PziGhcDqgb5fh+tfUkkJ2UCzEMIlSdIUAg/k4YCngB93sv9kApLUROBK4NHe6FQIoQC6ZLQNeEUEJSNdOgJWESCRXcD/Adf0Rt9JJI6uHKUOeUiSZAPeBIYDO4AC4A4hxGcDOKx+gSRJY4B3hRDTo+z7B/CZEOLF4PcdBCSrpDnhMMJhr4MQQngJvC2TCMdwoMLwvTK4LUkQhxGSS4wkYiGa++/hLW4ehkgSRBKxkLQkJJEkiCRi4m3gYimAhUBrUv9w+OGw10EcrpAk6UUCJt4hkiRVArcDFgj5WKwCTiFgSXABlw3MSJMYSBz2VowkkkgiNpJLjCSSSCImkgSRRBJJxESSIJJIIomYSBJEEkkkERNJgkgiiSRiIkkQSSSRREwkCSKJJJKIif8PkhsB8eYdeNgAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -582,9 +414,9 @@ "from collections import OrderedDict\n", "\n", "analyzer = bp.analysis.Bifurcation(\n", - " model=FN,\n", + " integrals=int_fhn,\n", " target_pars=OrderedDict(a=[0.5, 1.], Iext=[0., 1.]),\n", - " target_vars=OrderedDict(v=[-3, 3], w=[-3., 3.]),\n", + " target_vars=OrderedDict(V=[-3, 3], w=[-3., 3.]),\n", " numerical_resolution=0.01,\n", ")\n", "analyzer.plot_bifurcation(show=True)" @@ -607,9 +439,11 @@ "`brainpy.analysis.FastSlowBifurcation` is very usefull in the bursting neuron analysis. I will illustrate this by using the Hindmarsh-Rose model. The Hindmarsh–Rose model of neuronal activity is aimed to study the spiking-bursting behavior of the membrane potential observed in experiments made with a single neuron. Its dynamics are governed by:\n", "\n", "$$\n", - "\\frac{d V}{d t} = y - a V^3 + b V^2 - z + I\\\\\n", - "\\frac{d y}{d t} = c - d V^2 - y\\\\\n", - "\\frac{d z}{d t} = r (s (V - V_{rest}) - z)\n", + "\\begin{align}\n", + "\\frac{d V}{d t} &= y - a V^3 + b V^2 - z + I\\\\\n", + "\\frac{d y}{d t} &= c - d V^2 - y\\\\\n", + "\\frac{d z}{d t} &= r (s (V - V_{rest}) - z)\n", + "\\end{align}\n", "$$\n", "\n", "\n", @@ -626,40 +460,24 @@ }, { "cell_type": "code", - "execution_count": 17, - "metadata": {}, + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:58:29.650571Z", + "start_time": "2021-03-24T11:58:29.637572Z" + } + }, "outputs": [], "source": [ - "bp.profile.set(dt=0.02, numerical_method='rk4')\n", - "\n", "a = 1.; b = 3.; c = 1.; d = 5.; s = 4. \n", "x_r = -1.6; r = 0.001; Vth = 1.9\n", "\n", - "state = bp.types.NeuState('x', 'y', 'z', 'spike', 'input')\n", - "\n", - "@bp.integrate\n", - "def int_x(x, t, y, z, Isyn):\n", - " return y - a * x ** 3 + b * x * x - z + Isyn\n", - "\n", - "@bp.integrate\n", - "def int_y(y, t, x):\n", - " return c - d * x * x - y\n", - "\n", - "@bp.integrate\n", - "def int_z(z, t, x):\n", - " return r * (s * (x - x_r) - z)\n", - "\n", - "def update(ST, _t):\n", - " ST['y'] = int_y(ST['y'], _t, ST['x'])\n", - " ST['z'] = int_z(ST['z'], _t, ST['x'])\n", - " x = int_x(ST['x'], _t, ST['y'], ST['z'], ST['input'])\n", - " ST['spike'] = np.logical_and(x >= Vth, ST['x'] < Vth)\n", - " ST['x'] = x\n", - " ST['input'] = 0.\n", - "\n", - "\n", - "neuron = bp.NeuType(name='Hindmarsh_Rose_model',\n", - " ST=state, steps=update)" + "@bp.odeint(method='rk4', dt=0.02)\n", + "def int_hr(x, y, z, t, Isyn):\n", + " dx = y - a * x ** 3 + b * x * x - z + Isyn\n", + " dy = c - d * x * x - y\n", + " dz = r * (s * (x - x_r) - z)\n", + " return dx, dy, dz" ] }, { @@ -671,23 +489,30 @@ }, { "cell_type": "code", - "execution_count": 18, - "metadata": {}, + "execution_count": 8, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:58:37.282699Z", + "start_time": "2021-03-24T11:58:29.658573Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "SymPy solve \"int_y(x, y, z) = 0\" to \"y = f(x, z)\", success.\n", - "SymPy solve derivative of \"int_x(x, y, z)\" by \"x\", success.\n", - "SymPy solve derivative of \"int_x(x, y, z)\" by \"y\", success.\n", - "SymPy solve derivative of \"int_y(x, y, z)\" by \"x\", success.\n", - "SymPy solve derivative of \"int_y(x, y, z)\" by \"y\", success.\n" + "plot bifurcation ...\n", + "SymPy solve \"int_hr(x, y, z) = 0\" to \"y = f(x, z)\", success.\n", + "SymPy solve derivative of \"int_hr(x, y, z)\" by \"x\", success.\n", + "SymPy solve derivative of \"int_hr(x, y, z)\" by \"y\", success.\n", + "SymPy solve derivative of \"int_hr(x, y, z)\" by \"x\", success.\n", + "SymPy solve derivative of \"int_hr(x, y, z)\" by \"y\", success.\n", + "plot trajectory ...\n" ] }, { "data": { - "image/png": "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\n", + "image/png": "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Bjh8+DDt2rUjMjKS9PR0tm7d6nruhx9+cN045s0332TAgAHl9t+/f3++/PJL13LQx48fZ9OmTVXWNGjQIN59912OHz/OsWPHWLx4MQMHDmTYsGEsXLiQ/fv3A7imgGJiYsjIyADgvffeo6CgAICtW7fSrl077rzzTiZMmKAlpINU6bGCzm++Uet92MOHddwgxDl1DOBFoDHw6Zlvl19bayc7VEudREZGMn36dPr160eXLl3o2bNnte9ZsGABf//734mMjOTcc89l+vTpnH322Vx88cXExcVx1VVXMW3aNK677jpSUlJITEwst9/Y2Fhee+017rrrLrp168bdd99dbv9t27Zl3rx5jB492nUv4Keeesp1hzB3kpOTGTduHGlpaQBMnDiRpKQkAB599FEGDx5MREQESUlJzJs3jzvvvJPrr7+etLQ0hg0b5po6+uKLL3juueeIjIykWbNmzJ8/v2a/UAmopklJrmkdb5airkrZENAqpaFBy0GLBFCo/HvNf+ghjrxf/WjWWzp24CxPy0EHw1lAIhJkOj73HDz3nOtxXad4Kr5fgRAcFAAiUq2yHbYv5vsVCMFBASAiNVK2s97Qrz/28OE671OB4AwFgIjUWsXTQH11NlDF/Zw9cQLnlLnWRXxDASAiPlP2m/vumTM5MOdVn+z3wJxXy++rQQNi16/zyb7DmQJARPzinKlTy31r33TJMIp27PDNzouLK40SOr/5Bk3PnLos3lEAiEhAVLwuYOOAgRTv2+ez/W8dfUulbTqWUDUFgIg4osfy8ivF5t30X5xau9bDq2vH3TEJhcJPdEcwH8nPz2fBAo+LmnqUmZnJww8/7IeK3Bs/fjzt2rXzeFOZUp988gk9evTgggsu4Nlnn/VrTd60Fch6atOuU/XVJ13fXuhaoqK29zfwRtnlK8J9GQtdCewjr732GuvXr+f3v/99ue1FRUVEREQ4VFVlS5cupVmzZtx+++3k5OS4fU1RURHdu3fn008/pWPHjqSmpvLmm2/Sq1cvn9fjTVuBrKemtdW0vmD59xqqcnv1hjKLL/pTfRophMWVwE+8v471O3706T57tW/B49f1rvI1y5cv58EHH6RVq1b885//pFu3bnTq1InMzEyGDRvGY489xqJFi5g5cyYnTpygefPmLF68mLZt2zJmzBgmTpzIkCFDvK5p1apVTJgwgZUrV1JUVERaWhoLFiyo9ls9lCz6VrostScrV67kggsucK1QevPNN/Pee+957HB/85vfEB0dzZQpU4CStYPOOecc7r///mrr8aatmtYD8PLLL/Pyyy8DJQvrxcTEkJ6eXm09Na2ttvVJ7bg788df3+Dd7bdxfDxd317ol/acUK8CwCkDBgwgNTWVmTNnEhcXR8+ePYmNjS3X4QwdOpQbb7wRgCeeeIKFCxdy7733kp2dTUJCgtv9Xn311cyZM4f27duX256amsrw4cN57LHHOHHiBGPGjHF1/gMHDiy3qmipmTNncumlVa7Q7bJ9+3Y6derketyxY0dWrFjh8fUTJkxg5MiRTJkyheLiYt566y1WrlzpVT3etFXTegAmT57M5MmTKSgo4JJLLuHBBx/0qp7atFub+sR3Kn5T9+nZRhWcWru2Xh1XqFcBUN03dX/auHEjPXr04OTJkxw4cKDc3bcA5s2bx4IFCzh16hS7du3imWee4fTp0xw7dozGjRszduxYGjVqxJAhQ7j11luBkts2ejJ9+nRSU1Np0qQJL7zwgmu7L27B6G5a0N09AUrFxMTQpk0bMjMz2b17N0lJSbRp08arerxpq6b1lDVlyhQuueQSrrvuOq/qqU27dalPfM/dKqS5iUlQ4V4dvuR2FBIVRWzG6srbg0i9CgCn7N+/n5YtWxIZGUl2djb9+vWjYcOffrXz589n5cqVfP755zRr1oxBgwbRu3dv1q9fT2xsLP/4xz+48cYbue666xg1apQrAKpy4MABjh49SkFBASdPnnQtx+yLEUDHjh3Ztm2b63F+fn6lUUhFEydOZN68eezatYvx48e7tldXjzdt1aYeKAndrVu38uKLL3pVz8aNG/nLX/4ClISvt+3Wtj4JnNiszErb/B0KHDsW9NNICgAf+P77711/8GvXrq00pbN27VouuugimjVrxjvvvMNXX31FfHw8ixYtok+fPuTn5xMfHw/g9QHjSZMm8eSTT/L9998zbdo0VyfnixFAamoq3377Ld9//z0dOnTgrbfe4o03Sm4cMmzYMObPn0+HDh3KvWfEiBFMnz6dgoIC12u9qaeqtupST0ZGBjNnzmTZsmU0aPDTyW5V1XPppZdy7733uh63a9eu2tq8/QwSfNyFgj9ORa3I0zRSZNeuXPDRh35tuyIFgA/07NmTffv2ERcXR9u2bZk8ufy9bcaOHcv111/PokWLuPrqq+natStRUVGsWbOG/v37c+rUKfLz80lMTCx3e0lPxwDmz59Pw4YNueWWWygqKuKiiy7i888/55JLLqm21tGjR/PFF1+wb98+OnbsyBNPPMGECRPKtdWwYUNefPFFrrjiCoqKihg/fjy9e/emuLiYzZs3u70vcKNGjRg6dCitWrWq0VlPntqq+PlrWs+LL77IgQMHGDp0KAApKSnMmTPH67pqUltVr5PQ4umbeSBOFS3Iy3PfTqNGxGav8UubOg3UQcnJySxevJjo6Gjuu+8+mjRpwoABA7yaAnJCTk4Of/3rX5k1a1al54qLi0lOTubtt992e5P7QNcTrEL536uU56uVUGvi3Cdm0HrUqBq/LyxOAw0Vx44dY+DAgVx22WV07twZgLlz5zpcVfXi4uLcdrbr16/n2muvZcSIEQHr/KuqRyQQKq6EWsqfo4Vdj88AqFUIuKMAcEBUVFS9ull6r169yMvLc7oMkaDg6ZRQXwXDkX99qgAQEQklvgqG5pdf5otyAAWAiIijahIMtT0G4Em9CABrrS68kaAXSidciPMCcXVxyK8G2qRJE/bv368/Lglq1lr2799PkyZNnC5FxCXkRwAdO3YkPz+fvXv3Ol2KSJWaNGlCx44dnS5DxCXkAyAyMpIuXbo4XYaISMgJ+SkgERGpHQWAiEiYUgCIiISpkFoLyBizF9jqdB21EA3sc7qIAAq3zwv6zOEiVD9zZ2tt24obQyoAQpUxZrW7hZjqq3D7vKDPHC7q22fWFJCISJhSAIiIhCkFQGC84nQBARZunxf0mcNFvfrMOgYgIhKmNAIQEQlTCgARkTClAAggY8xUY4w1xkQ7XYu/GWOeM8ZsMMZkG2MWG2NaOV2TvxhjrjTGbDTGbDbGPOx0Pf5mjOlkjEk3xuQaY9YZY6Y4XVMgGGMijDGZxpgPnK7FVxQAAWKM6QRcBvzgdC0B8ikQZ61NADYBjzhcj18YYyKA2cBVQC9gtDGml7NV+V0h8N/W2ligP3BvGHxmgCmA/xfpDyAFQOD8AfgVEBZH3a21/7LWFp55+DVQX9dBTgM2W2vzrLWngbeA6x2uya+stTuttd+c+fkIJZ1iB2er8i9jTEfgGmCO07X4kgIgAIwxw4Ht1to1TtfikPHAx04X4ScdgG1lHudTzzvDsowxMUASsMLhUvzteUq+wBU7XIdPhfz9AIKFMWYJcK6bpx4Ffg1cHtiK/K+qz2ytfe/Max6lZMrg9UDWFkDu7kUaFqM8Y0wz4B3gAWvtj07X4y/GmGuBPdbaDGPMEIfL8SkFgI9Yay91t90YEw90AdacuW9xR+AbY0yatXZXAEv0OU+fuZQxZixwLTDM1t8LTvKBTmUedwR2OFRLwBhjIinp/F+31v7D6Xr87GJguDHmaqAJ0MIY83dr7RiH66ozXQgWYMaYLUCKtTYUVxT0mjHmSmAWMNhaW2/v12mMaUjJQe5hwHZgFXCLtXado4X5kSn5JvMacMBa+4DD5QTUmRHAVGvttQ6X4hM6BiD+8iLQHPjUGJNljHnZ6YL84cyB7vuAf1JyMHRhfe78z7gYuA245Mz/t1lnvh1LiNEIQEQkTGkEICISphQAIiJhSgEgIhKmFAAiImFKASAiEqYUACIiYUoBICISphQAInVgjJlc5mKo740x6U7XJOItXQgm4gNn1sb5HPgfa+37Ttcj4g2NAER844/A5+r8JZRoNVCROjLGjAM6U7ImkEjI0BSQSB0YY/pSsjLmQGvtQafrEakJTQGJ1M19wNlA+pkDwfXqloFSv2kEICISpjQCEBEJUwoAEZEwpQAQEQlTCgARkTClABARCVMKABGRMKUAEBEJU/8fAG/jFIL5KIUAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -699,7 +524,7 @@ }, { "data": { - "image/png": "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xahRo+jRowf33nuvY91bb71FbGwsMTExzJ4927F8yZIl9OzZkwsvvJA1a9Y4lh84cICrr76a/v37079//yrrVHCwbdoIzZq5XZ/d28b++fP9WJHyp0C45jAF+LT883OB3ZXW5ZcvcyIi00QkTUTSDhw4UO8i1u44xMniUkoNnCouZe2OQ/Xe52effUbHjh3ZsGEDmzZtYtSoUUyfPp3U1FQ2bdrE8ePH+eijjwC4+eabWbhwId99912Vfbz00ks0bdqUrKws5syZQ3p6utNxsrOzSU5OZs2aNWRmZhIWFsayZctc1pSZmUlycjIbN24kOTmZ3bt3s3fvXmbPns2qVavIzMwkNTWV//znP/z00088/PDDrFmzhi+++IItW7Y49jNjxgz+9Kc/kZqaynvvvcfUqdryORjZ0tNo1K6d2/UFrywmZ+AgP1ak/MVnzzmIyEqgg4tVc4wxH5RvMwcoBip+k7m6bcO42r8x5mXgZYDExESX29TGoO5taRzeiFPFpUSEN2JQ97b13SWxsbHMnDmT2bNnM2bMGIYOHcp7773H008/zbFjxygoKCA6Opphw4Zx+PBhLrzwQgBuuOEGPv20LC+//vpr7rrrLgDsdjt2u93pOF9++SXp6en0798fKBttnHXWWS5rSkpKomXLlgD06dOHvLw8Dh06xPDhw2nfvj0AkydP5uuvvwaosnz8+PHk5uYCsHLlyiph8euvv/Lbb7/RokWL+n3TVMDptfobdlx7HUUbN7pcb44cIdvWB1v2FpfrVcPks3Awxlxc3XoRuQkYAySZinMrZSOFTpU2iwJcP7rpZf26tGbZ1EGs3XGIQd3b0q9L63rvs2fPnqSnp/PJJ59w//33M3LkSF544QXS0tLo1KkTc+fO5cSJExhjqr2dsaZbHY0x3HTTTTz55JNVlr///vs88sgjALzyyisANGnSxLE+LCyM4uJifv/2e37s0tJSvvvuO84444xqa1PBofu77/BLcjL7Hp7regNj9GG5IGPV3UqjgNnAOGPMsUqrPgQmiEgTEekG9ADW+auufl1ac+eI87wSDAB79+6ladOmXH/99cycOZP169cD0K5dO44ePcry5csBaNWqFS1btmT16tUAVU4JDRs2zPH1pk2byMrKcjpOUlISy5cv5+effwagoKCAvLw8rrzySjIzM8nMzCQx0f1UkAMHDuSrr77i4MGDlJSU8NZbb3HhhRcycOBA/ve//3Ho0CFOnTrFu+++63jNyJEjef755x1fZ2Zm1vG7pBqK1uPH1/jLXy9UBw+r2mc8DzQBvij/y3StMeY2Y8xmEXkH2ELZ6aY7jTHOV1YbiI0bNzJr1iwaNWpEREQEL730Ev/5z3+IjY2la9eujtNAUHbhd8qUKTRt2pRLL/39Dt7bb7+dm2++GbvdTnx8PAMGOF+f79OnD4899hgjR46ktLSUiIgIXnjhBbp06eJRneeccw5PPvkkI0aMwBjDZZddxhVXXAHA3LlzGTx4MOeccw59+/Z1XOheuHAhd955J3a7neLiYoYNG8aiRYvq8+1SDYQtJ7vaENARRHCQ6k4pNBSJiYmm4t7/CtnZ2dhs+leMahga4s9rTaMEDYjAJyLpxhiXpxUC4W4lpVQD5MkpJp2XuuHScFBK1VlNAZE3cRLbLrvcT9Uob9JwUErVS00BcWrHDr1Q3QBpOCil6s2Wkw013HKtAdGwaDgopbzClr2FJrGx1W6jAdFwaDgopbym+7vv6LMQQULDwc+ee+45jh07VuN27tpxz507l/l+bnbWvHlzvx5PNXyeBETuRUl+qkbVhYZDJccyMjj4z5d9evudp+GgVENXU0CU7N2ro4gApuFQ7lhGBrtunsKBBQvYdfMUrwTE6S27H3nkEfbu3cuIESMYMWIEUPYEdGJiItHR0Tz88MNVXj9v3jwGDBjAgAED2LZtm9P+t2/fzqhRo+jXrx9Dhw4lJyfHZR3Nmzdnzpw5xMXFMWjQIPbv3w9AXl4eSUlJ2O12kpKS2LVrFwA//vgjgwcPpn///vzlL39xqql///7Y7XanepU6nScPwmlABCYNh3LH1qViTp6E0lLMqVMcW5da732e3rL77rvvpmPHjqSkpJCSkgLA448/TlpaGllZWXz11VdVeiedeeaZrFu3junTp3P33Xc77X/atGn84x//ID09nfnz53PHHXe4rKOwsJBBgwaxYcMGhg0bxr/+9S8Apk+fzo033khWVhaTJ092dH+dMWMGt99+O6mpqXTo8Htj3RUrVvDDDz+wbt06MjMzSU9Pd3RvVcodW042YR07VruNBkTg0XAo13RAf6RxYwgLQyIiaDqgf80vqkFsbCwrV65k9uzZfPPNN45W2ZW988479O3bl4SEBDZv3lylDfbEiRMd/54+z8PRo0f59ttvufbaax0T/Pz0008u62jcuDFjxowBoF+/fuzcuROA7777jkmTJgFlbcIrGv+tWbPGcewbbrjBsZ8VK1awYsUKEhIS6Nu3Lzk5Ofzwww91+daoENNz1ZceXYfYesFQP1WkamJV472A0zQhgc5LXuXYulSaDuhP04SEeu/TVcvuyn788Ufmz59PamoqrVu35o9//CMnTpxwrK/cLvv01tmlpaW0atXKqRtqSUkJ/fr1A2DcuHE8+uijREREOF5f0abbleqOB2Wtwe+//35uvfVWD969Us5qatpXevCgNu4LEDpyqKRpQgLtbp3mlWAA1y27W7RowW+//QaUTZDTrFkzWrZsyf79+x0T/FRITk52/Dt48OAq684880y6devmaKNtjGHDhg2EhYU52nQ/+uij1dZ3/vnn8/bbbwNlbcIvuOACAIYMGVJleYVLL72UV199laNHjwKwZ88eR5twpTyl1yEaBh05+JCrlt3fffcdo0eP5pxzziElJYWEhASio6Pp3r07Q4YMqfL6oqIiBg4cSGlpKW+99ZbT/pctW8btt9/OY489xqlTp5gwYQJxcXEe17dw4UKmTJnCvHnzaN++PUuWLAFgwYIFTJo0iQULFnD11Vc7th85ciTZ2dmOoGrevDlvvPGG21nnlHLHlpPN1guGUuridu0K2b1tNImNpfu77/ixMlVBW3YrFQBC+efVk1GCnmbyDW3ZrZQKWHqaKTBpOCilLOdpQOy49jo/VKNAw0EpFSBsOdm0GDum2m2KNm7UUYSfaDgopQJG1Lx5epopQGg4KKUCjqcBoQ/N+Y6Gg1IqINlysmucH6LioTnlfZaEg4jME5EcEckSkfdFpFX58q4iclxEMss/FllRny9Z0bL73XffxWazOZr9KdVQeDI/BJSNIvb7uZV9sLNq5PAFEGOMsQO5wP2V1m03xsSXf9zmz6L27ThC+mc72bfjiM+OYUXL7sWLF/Piiy86mv0p1dB4EhAFryzWUYQXWRIOxpgVxpiKBj9rgSgr6qhs344jfPD3DL7/YAcf/D3DKwERCC27H330UVavXs1tt93GrFmzOHHiBDfffDOxsbEkJCQ4AqOkpISZM2cSGxuL3W7nH//4B1B1BJOWlsbw4cMB+Oqrr4iPjyc+Pp6EhARHSxClfMWWk02bqbfUuJ0GhHcEQvuMKUBypa+7iUgG8CvwoDHmG1cvEpFpwDSAzp0717uIPbm/UFJcijFQUlLKntxf6NDduYtqbVS07P74448BOHLkCEuWLCElJYV27doBZS2727RpQ0lJCUlJSWRlZWG324HfW3a//vrr3H333Xz00UdV9j9t2jQWLVpEjx49+P7777njjjtYtWpVlW0eeughVq1axfz580lMTOSZZ54Bylp75OTkMHLkSHJzc1myZAk//vgjGRkZhIeHU1BQUO17mz9/Pi+88AJDhgzh6NGjREZG1ut7pZQnzp45k7NnzqwxALJ726BZM2zpadVup9zz2chBRFaKyCYXH1dU2mYOUAxUdHf7CehsjEkA7gHeFJEzXe3fGPOyMSbRGJPYvn37etd7bs/WhIU3QhpBWFgjzu3Zut77DJSW3ZWtXr3a0Ya7d+/edOnShdzcXFauXMltt91GeHjZ3wtt2rSpdj9DhgzhnnvuYeHChRw+fNjxOqX8wZaTDc2aVb9RYaGOIurBZ/9HG2Murm69iNwEjAGSTHmDJ2NMEVBU/nm6iGwHegI+j/8O3VtyxZ8S2JP7C+f2bF3vUQMETsvuytz10jLGuGzTHR4eTmlpKUCV2u677z4uv/xyPvnkEwYNGsTKlSvp3bu3u2+FUl5XMSrwZBQR1rEjPVd96Y+ygoZVdyuNAmYD44wxxyotby8iYeWfdwd6ADv8VVeH7i3pN6qrV4IBArNl97BhwxxtuHNzc9m1axe9evVi5MiRLFq0yDHXQ8Vppa5du5Keng7Ae++959jP9u3biY2NZfbs2SQmJrqdolQpX/PkYrXOV117Vt2t9DzQAvjitFtWhwFZIrIBWA7cZoyp/uR3ANu4cSMDBgwgPj6exx9/nAcffJBp06YxevRoRowYQVxcnKNl95QpU9y27F6wYAF///vfnfa/bNkyFi9eTFxcHNHR0XzwwQc11nTHHXdQUlJCbGws48ePZ+nSpTRp0oSpU6fSuXNn7HY7cXFxvPnmmwA8/PDDzJgxg6FDhxIWFubYz3PPPUdMTAxxcXGcccYZjB49up7fLaXqrjYXq/NumeqHiho+bdmtVADQn1fv8XSEoG3AtWW3UiqE2HKyCevYscbtsnvbyJ81yw8VNUwaDkqpoNNz1ZcejQx+++9Hei3CDQ0HpVTQsuVkIy5uIT+dXotwpuGglApqvb9f69Eo4tiaNTqKqETDQSkVEmw52eDBk/w641wZDQelVMiwZWZ4NIrQGec0HALe4cOHefHFF2vcbufOncTExLhcN3z4cE6/1bcms2bNIjo6mll6N4cKQrUZReQMHOSHigKPhkMlaUcKWZi3n7QjhVaX4uBpOHjbP//5T9avX8+8efP8fmyl/MHTUYQ5ciQkRxEaDuXSjhRyTeY2/rbjJ67J3OaVgDj9r/n58+czd+5chg8fzuzZsxkwYAA9e/bkm2/KGs9u3rzZ8US13W7nhx9+4L777mP79u3Ex8cza9Ysjh49SlJSEn379iU2NrbKU9HFxcXcdNNN2O12rrnmGpfzRqxYsYLBgwfTt29frr32Wo4ePeq0zbhx4ygsLGTgwIEkJyeTl5dHUlISdrudpKQkdu3aBcD+/fu58soriYuLIy4ujm+//dbtewZYuHAhffr0wW63M2HChHp/f5XyBo+a+FE2isiOdj06D0YaDuW+PXyUU6WGEuBUqeHbw86/NL2puLiYdevW8dxzz/HII48AsGjRImbMmEFmZiZpaWlERUXx1FNP8Yc//IHMzEzmzZtHZGQk77//PuvXryclJYU///nPjmZ6W7duZdq0aWRlZXHmmWc6jTgOHjzIY489xsqVK1m/fj2JiYk8++yzTrV9+OGHnHHGGWRmZjJ+/HimT5/OjTfeSFZWFpMnT+auu+4C4K677uLCCy9kw4YNrF+/nujo6Grf81NPPUVGRgZZWVksWhR0k/ypBsyWnubZE9MlJWT3tvFLcnLN2zZwGg7lzm/VnIhGQhgQ0Ug4v1Vznx7vqquuAqBfv37s3LkTgMGDB/PEE0/wt7/9jby8PM444wyn1xljeOCBB7Db7Vx88cXs2bOH/fv3A9CpUydHf6brr7+e1atXV3nt2rVr2bJlC0OGDCE+Pp7XXnuNvLy8Gmv97rvvmDRpEgA33HCDY7+rVq3i9ttvByAsLMxlS/LK7HY7kydP5o033tAW3yogefp09b6H5wb9qSYNh3KJLZuxPP48Znc/h+Xx55HYsuZhZk0qt7uGqi2vmzRpApT9Uq3ohDpp0iTHX+2XXnqp08Q9UNZs78CBA6Snp5OZmcnZZ5/t2O/pLbdP/9oYwyWXXOLo2rplyxYWL17M999/75jV7cMPP6zxfblq7e3Je/7444+58847SU9Pp1+/fo73rVQg8fTpaig71ZR7UZKPK7KGhkMliS2bcVeXs70SDABnn302P//8M4cOHaKoqMhpJrfT7dixg+7du3PXXXcxbtw4srKyqrT4hrLZ5M466ywiIiJISUmp8pf/rl27HJMCvfXWW1xwwQVV9j9o0CDWrFnjmHL02LFj5ObmMnDgQEdgjBs3zqmu888/n7fffhsoC6eK/SYlJfHSSy8BZfNI/Prrr27fc2lpKbt372bEiBE8/fTTHD582OX1DqUChS0nmxZjx9S4XbC2A9dw8KGIiAgeeughBg4cyJgxY2qcDCc5OZmYmBji4+PJycnhxhtvpG3btgwZMoSYmBhmzZrF5MmTSUtLIzExkWXLllXZp81m47XXXsNut1NQUOA45VOhffv2LF26lIkTJ2K32xk0aJBH8zAsXLiQJUuWYLfb+fe//82CBQsAWLBgASkpKcTGxtKvXz82b97s9j2XlJRw/fXXO+au/tOf/kSrVq1q+R1Vyr+i5s2r1SgimC5Ya8tupQKA/rw2DJ6OEFqMHUNUA7gNXFt2K6WUF3j68FwwdHvVcFBKqVrw9OE5KD/VFBPr44p8Q8NBKaXqwJaTTdPTpvZ1qbiY7N429s+f7/uivEjDQSml6qjL4lfKRhHV3N5doeCVxQ3qVJOGg1JK1ZMte0vQ3dWk4aCUUl7i6RPWFW04Ann2OQ0HpZTyoto8YR3Is89ZEg4i8lcRyRKRTBFZISIdK627X0S2ichWEbnUivqUUqq+bDnZtTvVFGAhYdXIYZ4xxm6MiQc+Ah4CEJE+wAQgGhgFvCgiYRbV6BX5+fkk16GDY0ZGBvfdd58PKnJtypQpnHXWWW4nDKrw2Wef0atXL8477zyeeuopn9bkybH8WU9djmtVfSpw2HKyieje3aNtA2pyIWOMpR/A/cBLlT6/v9K6z4HBNe2jX79+5nRbtmxxWmaFpUuXmnvvvddpeXFxsQXVuPfVV1+Z9PR0Ex0d7Xab4uJi0717d7N9+3ZTVFRk7Ha72bx5s0/q8eRY/qynLsetTX2B8vOqfGtLr94efxSuX+/zeoA04+b3qmV9k0XkceBG4AgwonzxucDaSpvlly9z9fppwDSAzp07V3usR/67mS17f61nxVX16XgmD4+tfv6C1atXc88999CqVSs+//xzevToQadOncjIyCApKYkHH3yQ5cuXM3/+fI4fP06LFi14//33ad++Pddffz1Tp05l+PDhHteUmprKLbfcwrp16ygpKWHAgAGOfk01GTZsmKN1uDvr1q3jvPPOo3v5X0ETJkzggw8+oE+fPi63/8tf/kK7du2YMWMGAHPmzOHss892zAdR32PVth4omzOjYi6JI0eO0LVrV1JSUmqsp7a11bU+FdwqTjN5cgopb+KkKq/xN5+dVhKRlSKyycXHFQDGmDnGmE7AMmB6xctc7Mpl8ydjzMvGmERjTGL79u198ybq6YILLqB///588MEHZGZmsnHjRpo3b05KSgoPPvggACNGjGDt2rVs2LCBSy65hHfeeQeArKws7Ha7y/1edtll7N2712l5//79GTduHA8++CD33nsv119/vSMYhg4d6mjLXflj5cqVHr+fPXv20KlTJ8fXUVFR7Nmzx+32t9xyC6+99hpQ1pX17bffZvLkyR7V48mxalsPwG233UZmZiapqalERUVxzz33eFRPXY5bl/pUaPD4ATqsux7hs5GDMeZiDzd9E/gYeJiykUKnSuuiAOffgrVU01/4vrR161Z69erFiRMnKCgo4KGHHqqyfunSpSQnJ1NUVMS+fft44oknOHnyJIWFhTRp0oSbbrqJxo0bM3z4cMcv1k8++cTt8R566CH69+9PZGQkCxcudCyvmIq0PoyLJo3Vze3QtWtX2rZtS0ZGBvv37ychIYG2bdt6VI8nx6ptPZXNmDGDiy66iLFjx3pUT12OW5/6VPDrsvgVgLL2Gh7MbZLd24a0bEnv79fWuK03WHJaSUR6GGN+KP9yHFDRN/pD4E0ReRboCPQA1llQolccOnSIli1bEhERQVZWFgMHDqwyA9rrr7/OunXrWLVqFc2bN2fYsGFER0ezZcsWbDYb//d//8c111zD2LFjGT9+vCMcqlNQUMDRo0c5deoUJ06coFn53LhDhw6tMi9Ehfnz53PxxZ7leFRUFLt373Z8nZ+fT8ca7umeOnUqS5cuZd++fUyZMsWxvKZ6PDlWXeqBskDOy8vj+eef96ierVu38q9//QsoC2ZPj1vX+lRosW3aCHh2qskcOUJ2bxsdHplL6/HjfVuYu4sRvvwA3gM2AVnAf4FzK62bA2wHtgKjPdlfoF6QTk1NNaNHjzbGGLNkyRLzwAMPVFk/c+ZM89xzzxljjFm+fLkJCwszR48eNUuXLjUPPPCAeeKJJ0xGRoYxxpiJEyd6dMyxY8eaZcuWmccee8zceeedtar3xx9/rPaC9KlTp0y3bt3Mjh07HBdYN23aZIwx5qKLLjL5+flOrykqKjI9e/Y03bp1q9VF+OqOVZ960tLSTHR0tCkoKPC4lrrUVpvtjAmMn1cVGGpz0bq+qOaCtCW3shpjrjbGxJiy21nHGmP2VFr3uDHmD8aYXsaYT62oz1t69+7NwYMHiYmJcUzCU9lNN93EwoULGTp0KLm5uXTv3p1mzZqxYcMG4uLiiIqKIj8/H6DK1Jvurjm8/vrrhIeHM2nSJO677z5SU1NdTjXqysSJExk8eDBbt24lKiqKxYsXOx0rPDyc559/nksvvRSbzcZ1111HdHQ0paWlbNu2jTZt2jjtt3HjxowYMYLrrruOsDDP70p2d6zKNdWlnueff56CggJGjBhBfHw8U6fW/glVT2qraTul3Kntra++uh6hk/0EoL59+/L+++/Trl07pk+fTmRkJBdccIFHp5WssGnTJl599VWeffZZp3WlpaX07duXd999lx49elheT6BqyD+vyneyo2OgpMSjbetyV1N1k/1oOASQwsJChg4dyiWXXMLf/vY3q8upty1btjBmzBiuvPJKnnnmGavLCWgN8edV+Y9Ho4PwcMf1C09VFw6WPeegnDVr1oz169dbXYbX9OnThx07dlhdhlINnkfPR3hwx1NtaOM9pZRqIGw52bQYO8b1ynDv/q2v4aCUUg1I1Lx5ZSOJ8tvUgTqdUqpJUJ9WMsboQ0cq4AXDdT/lf7b0tJo3qoegHTlERkZy6NAh/R9PBTRjDIcOHSIyMtLqUpSqImhHDhXPCBw4cMDqUpSqVmRkJFFRUVaXoVQVQRsOERERdOvWzeoylFKqQQra00pKKaXqTsNBKaWUEw0HpZRSToKifYaIHADyrK6jDtoBB60uws/0PYeGUHvPDfX9djHGuJwtLSjCoaESkTR3fU2Clb7n0BBq7zkY36+eVlJKKeVEw0EppZQTDQdrvWx1ARbQ9xwaQu09B9371WsOSimlnOjIQSmllBMNB6WUUk40HAKEiMwUESMi7ayuxddEZJ6I5IhIloi8LyKtrK7JF0RklIhsFZFtInKf1fX4moh0EpEUEckWkc0iMsPqmvxFRMJEJENEPrK6Fm/RcAgAItIJuATYZXUtfvIFEGOMsQO5wP0W1+N1IhIGvACMBvoAE0Wkj7VV+Vwx8GdjjA0YBNwZAu+5wgwg2+oivEnDITD8HbgXCIm7A4wxK4wxFRPergWCsV/1AGCbMWaHMeYk8DZwhcU1+ZQx5idjzPryz3+j7JfludZW5XsiEgVcDrxidS3epOFgMREZB+wxxmywuhaLTAE+tboIHzgX2F3p63xC4BdlBRHpCiQA31tcij88R9kfd6UW1+FVQTufQyARkZVABxer5gAPACP9W5HvVfeejTEflG8zh7JTEcv8WZufuJqfNiRGhiLSHHgPuNsY86vV9fiSiIwBfjbGpIvIcIvL8SoNBz8wxlzsarmIxALdgA3lc11HAetFZIAxZp8fS/Q6d++5gojcBIwBkkxwPmyTD3Sq9HUUsNeiWvxGRCIoC4Zlxpj/s7oePxgCjBORy4BI4EwRecMYc73FddWbPgQXQERkJ5BojGmI3R09JiKjgGeBC40xQTmPq4iEU3axPQnYA6QCk4wxmy0tzIek7C+c14ACY8zdFpfjd+Ujh5nGmDEWl+IVes1BWeF5oAXwhYhkisgiqwvytvIL7tOBzym7MPtOMAdDuSHADcBF5f9dM8v/olYNkI4clFJKOdGRg1JKKScaDkoppZxoOCillHKi4aCUUsqJhoNSSiknGg5KKaWcaDgopZRyouGglI+IyG2VHgb7UURSrK5JKU/pQ3BK+Vh5v6FVwNPGmP9aXY9SntCRg1K+twBYpcGgGhLtyqqUD4nIH4EulPVZUqrB0NNKSvmIiPSjrEvpUGPML1bXo1Rt6GklpXxnOtAGSCm/KB1U00iq4KYjB6WUUk505KCUUsqJhoNSSiknGg5KKaWcaDgopZRyouGglFLKiYaDUkopJxoOSimlnPw/vJIp/cNVeOwAAAAASUVORK5CYII=\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -712,15 +537,15 @@ ], "source": [ "analyzer = bp.analysis.FastSlowBifurcation(\n", - " model=neuron,\n", + " integrals=int_hr,\n", " fast_vars={'x': [-3, 3], 'y': [-10., 5.]},\n", " slow_vars={'z': [-5., 5.]},\n", - " fixed_vars={'Isyn': 0.5, 'input': 0.5, },\n", + " pars_update={'Isyn': 0.5},\n", " numerical_resolution=0.001\n", ")\n", "analyzer.plot_bifurcation()\n", "analyzer.plot_trajectory([{'x': 1., 'y': 0., 'z': -0.0}],\n", - " duration=30.,\n", + " duration=100.,\n", " show=True)" ] }, @@ -768,7 +593,10 @@ }, "toc": { "base_numbering": 1, - "nav_menu": {}, + "nav_menu": { + "height": "211px", + "width": "348px" + }, "number_sections": false, "sideBar": true, "skip_h1_title": false, diff --git a/docs/tutorials/neurodynamics_simulation.ipynb b/docs/tutorials/neurodynamics_simulation.ipynb index 259147c9..6162da3f 100644 --- a/docs/tutorials/neurodynamics_simulation.ipynb +++ b/docs/tutorials/neurodynamics_simulation.ipynb @@ -2,43 +2,1040 @@ "cells": [ { "cell_type": "markdown", - "id": "overall-angola", "metadata": {}, "source": [ "# Neurodynamics Simulation" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Contents**\n", + "\n", + "- [brainpy.NeuGroup](#brainpy.NeuGroup)\n", + "- [brainpy.TwoEndConn](#brainpy.TwoEndConn)\n", + "- [brainpy.Network](#brainpy.Network)\n", + "- [brainpy.connect module](#brainpy.connect-module)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For brain modeling, BrainPy provides the interface of `brainpy.NeuGroup`, `brainpy.TwoEndConn`, and `brainpy.Network` for convenient neurodynamics simulation." + ] + }, { "cell_type": "code", - "execution_count": null, - "id": "union-infrared", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:48.939126Z", + "start_time": "2021-03-25T03:02:47.073698Z" + } + }, + "outputs": [], + "source": [ + "import brainpy as bp\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## brainpy.NeuGroup" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`brainpy.NeuGroup` is used for neuron group modeling. User-defined neuron group models must inherit from the `brainpy.NeuGroup`. Let's take the [leaky integrate-and-fire](https://en.wikipedia.org/wiki/Biological_neuron_model#Leaky_integrate-and-fire) (LIF) model and [Hodgkin–Huxley neuron model](https://en.wikipedia.org/wiki/Hodgkin%E2%80%93Huxley_model) as the illustrated examples. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### LIF model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The formal equations of a LIF model is given by:\n", + "\n", + "$$\n", + "\\tau_m \\frac{dV}{dt} = - (V(t) - V_{rest}) + I(t) \n", + "\\\\\n", + "\\text{after}\\, V(t) \\gt V_{th}, V(t) =V_{rest}\n", + "\\,\n", + "\\text{last}\\, \\tau_{ref}\\, \\text{ms} \n", + "$$\n", + "\n", + "where $V$ is the membrane potential, $V_{rest}$ is the rest membrane potential, $V_{th}$ is the spike threshold, $\\tau_m$ is the time constant, $\\tau_{ref}$ is the refractory time period, and $I$ is the time-variant synaptic inputs. " + ] + }, + { + "cell_type": "markdown", "metadata": {}, + "source": [ + "As stated above, the numerical integration of the differential equation in LIF model can be coded as:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:48.959138Z", + "start_time": "2021-03-25T03:02:48.941426Z" + } + }, "outputs": [], - "source": [] + "source": [ + "@bp.odeint\n", + "def int_V(V, t, Iext, V_rest, R, tau):\n", + " return (- (V - V_rest) + R * Iext) / tau" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, we will define the following items to store the neuron state:\n", + "\n", + "- ``V``: The membrane potential.\n", + "- ``input``: The synaptic input.\n", + "- ``spike``: Whether produce a spike.\n", + "- ``refractory``: Whether the neuron is in refractory state.\n", + "- ``t_last_spike``: The last spike time for calculating refractory state.\n", + "\n", + "Based on these states, the updating logic of LIF model from the current time $t$ to the next time $t+dt$ will be coded as:" + ] }, { "cell_type": "code", - "execution_count": null, - "id": "corporate-trunk", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:48.995265Z", + "start_time": "2021-03-25T03:02:48.969008Z" + } + }, + "outputs": [], + "source": [ + "class LIF(bp.NeuGroup):\n", + " target_backend = ['numpy', 'numba']\n", + "\n", + " def __init__(self, size, t_refractory=1., V_rest=0.,\n", + " V_reset=-5., V_th=20., R=1., tau=10., **kwargs):\n", + " # parameters\n", + " self.V_rest = V_rest\n", + " self.V_reset = V_reset\n", + " self.V_th = V_th\n", + " self.R = R\n", + " self.tau = tau\n", + " self.t_refractory = t_refractory\n", + "\n", + " # variables\n", + " self.t_last_spike = bp.backend.ones(size) * -1e7\n", + " self.refractory = bp.backend.zeros(size)\n", + " self.input = bp.backend.zeros(size)\n", + " self.spike = bp.backend.zeros(size)\n", + " self.V = bp.backend.ones(size) * V_reset\n", + "\n", + " super(LIF, self).__init__(size=size, **kwargs)\n", + " \n", + " @staticmethod\n", + " @bp.odeint\n", + " def int_V(V, t, Iext, V_rest, R, tau):\n", + " return (- (V - V_rest) + R * Iext) / tau\n", + " \n", + " def update(self, _t):\n", + " for i in range(self.size[0]):\n", + " if _t - self.t_last_spike[i] <= self.t_refractory:\n", + " self.refractory[i] = 1.\n", + " else:\n", + " self.refractory[0] = 0.\n", + " V = self.int_V(self.V[i], _t, self.input[i], self.V_rest, self.R, self.tau)\n", + " if V >= self.V_th:\n", + " self.V[i] = self.V_reset\n", + " self.spike[i] = 1.\n", + " self.t_last_spike[i] = _t\n", + " else:\n", + " self.spike[i] = 0.\n", + " self.V[i] = V\n", + " self.input[i] = 0." + ] + }, + { + "cell_type": "markdown", "metadata": {}, + "source": [ + "That's all, we have coded a LIF neuron model. \n", + "\n", + "Each NeuGroup has a powerful function: ``.run()``. In this function, it receives the following arguments:\n", + "\n", + "- ``duration``: Specify the simulation duration. Can be a tuple with ``(start time, end time)``. Or it can be a int to specify the duration ``length`` (then the default start time is ``0``).\n", + "- ``inputs``: Specify the inputs for each model component. With the format of ``(target, value, [operation])``. The default operation is ``+``, which means the input ``value`` will be added to the ``target``. Or, the operation can be ``-``, ``*``, ``/``, or ``=``.\n", + "\n", + "Now, let's run it." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:49.010946Z", + "start_time": "2021-03-25T03:02:49.000504Z" + } + }, "outputs": [], - "source": [] + "source": [ + "group = LIF(100, monitors=['V'])" + ] }, { "cell_type": "code", - "execution_count": null, - "id": "photographic-acrylic", + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:49.941507Z", + "start_time": "2021-03-25T03:02:49.014011Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Compilation used 0.0011 s.\n", + "Start running ...\n", + "Run 10.0% used 0.091 s.\n", + "Run 20.0% used 0.167 s.\n", + "Run 30.0% used 0.259 s.\n", + "Run 40.0% used 0.334 s.\n", + "Run 50.0% used 0.408 s.\n", + "Run 60.0% used 0.473 s.\n", + "Run 70.0% used 0.538 s.\n", + "Run 80.0% used 0.605 s.\n", + "Run 90.0% used 0.680 s.\n", + "Run 100.0% used 0.747 s.\n", + "Simulation is done in 0.747 s.\n", + "\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "group.run(duration=200., inputs=('input', 26.), report=True)\n", + "bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:50.811867Z", + "start_time": "2021-03-25T03:02:49.944594Z" + }, + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Compilation used 0.0000 s.\n", + "Start running ...\n", + "Run 10.0% used 0.074 s.\n", + "Run 20.0% used 0.148 s.\n", + "Run 30.0% used 0.219 s.\n", + "Run 40.0% used 0.283 s.\n", + "Run 50.0% used 0.367 s.\n", + "Run 60.0% used 0.452 s.\n", + "Run 70.0% used 0.520 s.\n", + "Run 80.0% used 0.588 s.\n", + "Run 90.0% used 0.656 s.\n", + "Run 100.0% used 0.735 s.\n", + "Simulation is done in 0.735 s.\n", + "\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "group.run(duration=(200, 400.), report=True)\n", + "bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you experenced just now, the benifit of inheriting `brainpy.NeuGroup` lies at the following several ways:\n", + "\n", + "- Easy way to monitor variable trajectories.\n", + "- Powerfull \"inputs\" support.\n", + "- Continuous running support. \n", + "- Progress report support. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "On the model definition, BrainPy endows you the fully data/logic flow control. You can define models with any data you need and any logic you want. There are little limitations/constrains on your customization. 1, you should set what computing backend do your defined model support by the keyword `target_backend`. 2, you should \"super()\" initialize the `brainpy.NeuGroup` with the keyword of the group `size`. 3, you should define the `update` function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Hodgkin–Huxley model" + ] + }, + { + "cell_type": "markdown", "metadata": {}, + "source": [ + "The updating logic in the above LIF model is coded with a for loop, which is very suitable for Numba backend (because Numba is a Just-In-Time compiler, and it is good at the for loop optimization). However, for array-oriented programming languages, such as NumPy, PyTorch and TensorFlow, this coding schema is inefficient. Here, let's use the HH neuron model as example to demonstrate how to code an array-based neuron model for general backends." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:50.837658Z", + "start_time": "2021-03-25T03:02:50.814760Z" + } + }, "outputs": [], - "source": [] + "source": [ + "class HH(bp.NeuGroup):\n", + " target_backend = 'general'\n", + "\n", + " def __init__(self, size, ENa=50., EK=-77., EL=-54.387,\n", + " C=1.0, gNa=120., gK=36., gL=0.03, V_th=20.,\n", + " **kwargs):\n", + " # parameters\n", + " self.ENa = ENa\n", + " self.EK = EK\n", + " self.EL = EL\n", + " self.C = C\n", + " self.gNa = gNa\n", + " self.gK = gK\n", + " self.gL = gL\n", + " self.V_th = V_th\n", + "\n", + " # variables\n", + " self.V = bp.backend.ones(size) * -65.\n", + " self.m = bp.backend.ones(size) * 0.5\n", + " self.h = bp.backend.ones(size) * 0.6\n", + " self.n = bp.backend.ones(size) * 0.32\n", + " self.spike = bp.backend.zeros(size)\n", + " self.input = bp.backend.zeros(size)\n", + " \n", + " def diff(V, m, h, n, t, Iext, gNa, ENa, gK, EK, gL, EL, C):\n", + " alpha = 0.1 * (V + 40) / (1 - bp.backend.exp(-(V + 40) / 10))\n", + " beta = 4.0 * bp.backend.exp(-(V + 65) / 18)\n", + " dmdt = alpha * (1 - m) - beta * m\n", + "\n", + " alpha = 0.07 * bp.backend.exp(-(V + 65) / 20.)\n", + " beta = 1 / (1 + bp.backend.exp(-(V + 35) / 10))\n", + " dhdt = alpha * (1 - h) - beta * h\n", + "\n", + " alpha = 0.01 * (V + 55) / (1 - bp.backend.exp(-(V + 55) / 10))\n", + " beta = 0.125 * bp.backend.exp(-(V + 65) / 80)\n", + " dndt = alpha * (1 - n) - beta * n\n", + "\n", + " I_Na = (gNa * m ** 3.0 * h) * (V - ENa)\n", + " I_K = (gK * n ** 4.0) * (V - EK)\n", + " I_leak = gL * (V - EL)\n", + " dVdt = (- I_Na - I_K - I_leak + Iext) / C\n", + "\n", + " return dVdt, dmdt, dhdt, dndt\n", + " \n", + " self.integral = bp.odeint(f=diff, method='rk4', dt=0.01)\n", + " \n", + " super(HH, self).__init__(size=size, **kwargs)\n", + "\n", + " def update(self, _t):\n", + " V, m, h, n = self.integral(self.V, self.m, self.h, self.n, _t,\n", + " self.input, self.gNa, self.ENa, self.gK,\n", + " self.EK, self.gL, self.EL, self.C)\n", + " self.spike = (self.V < self.V_th) * (V >= self.V_th)\n", + " self.V = V\n", + " self.m = m\n", + " self.h = h\n", + " self.n = n\n", + " self.input[:] = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In HH example, all the operations (including \"zeros\", \"ones\" and \"exp\") are used from the `brainpy.backend` as `bp.backend.zeros`, `bp.backend.ones` and `bp.backend.exp`. What's more, we set the \"target_backend\" as `general`. So, let's try to run this model on various backends." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First is PyTorch." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:02:58.556910Z", + "start_time": "2021-03-25T03:02:50.842667Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "bp.backend.set('pytorch')\n", + "\n", + "group = HH(100, monitors=['V'])\n", + "group.run(200., inputs=('input', 10.))\n", + "bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Second is NumPy." + ] }, { "cell_type": "code", - "execution_count": null, - "id": "biological-tsunami", + "execution_count": 9, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:00.330366Z", + "start_time": "2021-03-25T03:02:58.561102Z" + }, + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "bp.backend.set('numpy')\n", + "\n", + "group = HH(100, monitors=['V'])\n", + "group.run(200., inputs=('input', 10.))\n", + "bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True)" + ] + }, + { + "cell_type": "markdown", "metadata": {}, + "source": [ + "The last is Numba." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:12.362447Z", + "start_time": "2021-03-25T03:03:00.335509Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "bp.backend.set('numba')\n", + "\n", + "group = HH(100, monitors=['V'])\n", + "group.run(200., inputs=('input', 10.))\n", + "bp.visualize.line_plot(group.mon.ts, group.mon.V, show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## brainpy.TwoEndConn" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For synaptic connections, BrainPy provides `brainpy.TwoEndConn` to help you construct the projection between pre-synaptic and post-synaptic neuron groups, and provides `brainpy.connect.Connector` for synaptic connectivity between pre- and post- groups. \n", + "\n", + "- The benifit of using `brainpy.TwoEndConn` lies at the **automatical synaptic delay**. The synapse modeling usually includes a delay time (typically 0.3–0.5 ms) required for a neurotransmitter to be released from a presynaptic membrane, diffuse across the synaptic cleft, and bind to a receptor site on the post-synaptic membrane. BrainPy provides `register_constant_dely()` for automatical state delay. \n", + "\n", + "- The benifit of using `brainpy.connect.Connector` lies at the **connectivity structure construction**. `brainpy.connect.Connector` provides various synaptic structures, like \"pre_ids\", \"post_ids\", \"conn_mat\", \"pre2post\", \"post2pre\", \"pre2syn\", \"post2syn\", \"pre_slice_syn\", and \"post_slice_syn\". Users can \"requires\" such data structures by calling `connector.requires('pre_ids', 'post_ids', ...)`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here, let's illustrate this by the AMPA synapse model." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### AMPA Synapse Model" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:12.401462Z", + "start_time": "2021-03-25T03:03:12.369072Z" + } + }, + "outputs": [], + "source": [ + "class AMPA(bp.TwoEndConn):\n", + " target_backend = ['numpy', 'numba']\n", + "\n", + " def __init__(self, pre, post, conn, delay=0., g_max=0.10, E=0., tau=2.0, **kwargs):\n", + " # parameters\n", + " self.g_max = g_max\n", + " self.E = E\n", + " self.tau = tau\n", + " self.delay = delay\n", + "\n", + " # connections\n", + " self.conn = conn(pre.size, post.size)\n", + " self.conn_mat = conn.requires('conn_mat')\n", + " self.size = bp.backend.shape(self.conn_mat)\n", + "\n", + " # variables\n", + " self.s = bp.backend.zeros(self.size)\n", + " self.g = self.register_constant_delay('g', size=self.size, delay_time=delay)\n", + "\n", + " super(AMPA, self).__init__(pre=pre, post=post, **kwargs)\n", + "\n", + " @staticmethod\n", + " @bp.odeint(dt=0.01)\n", + " def int_s(s, t, tau):\n", + " return - s / tau\n", + "\n", + " def update(self, _t):\n", + " self.s = self.int_s(self.s, _t, self.tau)\n", + " for i in range(self.pre.size[0]):\n", + " if self.pre.spike[i] > 0:\n", + " self.s[i] += self.conn_mat[i]\n", + " self.g.push(self.g_max * self.s)\n", + " g = self.g.pull()\n", + " self.post.input -= bp.backend.sum(g, axis=0) * (self.post.V - self.E)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To define a two-end projection synapse is very much like the NeuGroup. Users need to inherit the `brainpy.TwoEndConn`, and provide the \"target_backend\" specification, \"update\" function and then \"super()\" initialize the parent class. But what different are two aspects: 1. connection. We need construct the synaptic connectivity by \"connector.requires\". 2. delay. We can register a constant delay variable by \"self.register_constant_delay()\"." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here, we create a matrix-based connectivity (with the shape of `(num_pre, num_post)`). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And then register a delay variable \"self.g\" with the shape of `(num_pre, num_post)`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## brainpy.Network" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's put the above defined HH model and AMPA synapse together to construct a network with `brainpy.Network`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:12.416738Z", + "start_time": "2021-03-25T03:03:12.406188Z" + } + }, + "outputs": [], + "source": [ + "bp.backend.set('numpy')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:12.440969Z", + "start_time": "2021-03-25T03:03:12.421049Z" + } + }, + "outputs": [], + "source": [ + "group = HH(10, monitors=['V', 'spike'])\n", + "syn = AMPA(pre=group, post=group, conn=bp.connect.All2All(), delay=1.5, monitors=['s'])" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:14.861988Z", + "start_time": "2021-03-25T03:03:12.443012Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Compilation used 0.8206 s.\n", + "Start running ...\n", + "Run 10.0% used 0.188 s.\n", + "Run 20.0% used 0.342 s.\n", + "Run 30.0% used 0.506 s.\n", + "Run 40.0% used 0.643 s.\n", + "Run 50.0% used 0.771 s.\n", + "Run 60.0% used 0.896 s.\n", + "Run 70.0% used 1.098 s.\n", + "Run 80.0% used 1.282 s.\n", + "Run 90.0% used 1.431 s.\n", + "Run 100.0% used 1.585 s.\n", + "Simulation is done in 1.585 s.\n", + "\n" + ] + } + ], + "source": [ + "net = bp.Network(group, syn)\n", + "net.run(duration=200., inputs=(group, \"input\", 20.), report=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:15.584605Z", + "start_time": "2021-03-25T03:03:14.865850Z" + }, + "scrolled": false + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig, gs = bp.visualize.get_figure(2, 1, 3, 8)\n", + "\n", + "fig.add_subplot(gs[0, 0])\n", + "bp.visualize.line_plot(group.mon.ts, group.mon.V, legend='pre-V')\n", + "\n", + "fig.add_subplot(gs[1, 0])\n", + "bp.visualize.line_plot(syn.mon.ts, syn.mon.s, legend='syn-s', show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## brainpy.connect module" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "BrainPy provides several commonly used connection methods in ``brainpy.connect`` module (see the follows). They are all inherited from the base class `brainpy.connect.Connector`. Users can also customize their synaptic connectivity by class inheritance. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.One2One\n", + "\n", + "The neurons in the pre-synaptic neuron group only connect to the neurons\n", + "in the same position of the post-synaptic group. Thus, this connection\n", + "requires the indices of two neuron groups same. Otherwise, an error will\n", + "occurs.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.All2All\n", + "\n", + "All neurons of the post-synaptic population form connections with all\n", + "neurons of the pre-synaptic population (dense connectivity). Users can\n", + "choose whether connect the neurons at the same position\n", + "(`include_self=True or False`).\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.GridFour\n", + "\n", + "`GridFour` is the four nearest neighbors connection. Each neuron connect to its\n", + "nearest four neurons.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.GridEight\n", + "\n", + "`GridEight` is eight nearest neighbors connection. Each neuron connect to its\n", + "nearest eight neurons.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.GridN\n", + "\n", + "`GridN` is also a nearest neighbors connection. Each neuron connect to its\n", + "nearest $2N \\cdot 2N$ neurons.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.FixedProb\n", + "\n", + "For each post-synaptic neuron, there is a fixed probability that it forms a connection\n", + "with a neuron of the pre-synaptic population. It is basically a all_to_all projection,\n", + "except some synapses are not created, making the projection sparser.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.FixedPreNum\n", + "\n", + "Each neuron in the post-synaptic population receives connections from a\n", + "fixed number of neurons of the pre-synaptic population chosen randomly.\n", + "It may happen that two post-synaptic neurons are connected to the same\n", + "pre-synaptic neuron and that some pre-synaptic neurons are connected to\n", + "nothing.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.FixedPostNum\n", + "\n", + "Each neuron in the pre-synaptic population sends a connection to a fixed number of neurons\n", + "of the post-synaptic population chosen randomly. It may happen that two pre-synaptic neurons\n", + "are connected to the same post-synaptic neuron and that some post-synaptic neurons receive\n", + "no connection at all.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.GaussianProb\n", + "\n", + "\n", + "Builds a Gaussian connection pattern between the two populations, where\n", + "the connection probability decay according to the gaussian function.\n", + "\n", + "Specifically,\n", + "\n", + "$$\n", + "p=\\exp(-\\frac{(x-x_c)^2+(y-y_c)^2}{2\\sigma^2})\n", + "$$\n", + "\n", + "where $(x, y)$ is the position of the pre-synaptic neuron\n", + "and $(x_c,y_c)$ is the position of the post-synaptic neuron.\n", + "\n", + "For example, in a $30 \\textrm{x} 30$ two-dimensional networks, when\n", + "$\\beta = \\frac{1}{2\\sigma^2} = 0.1$, the connection pattern is shown\n", + "as the follows:\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.GaussianWeight\n", + "\n", + "Builds a Gaussian connection pattern between the two populations, where\n", + "the weights decay with gaussian function.\n", + "\n", + "Specifically,\n", + "\n", + "$$w(x, y) = w_{max} \\cdot \\exp(-\\frac{(x-x_c)^2+(y-y_c)^2}{2\\sigma^2})$$\n", + "\n", + "where $(x, y)$ is the position of the pre-synaptic neuron (normalized\n", + "to [0,1]) and $(x_c,y_c)$ is the position of the post-synaptic neuron\n", + "(normalized to [0,1]), $w_{max}$ is the maximum weight. In order to void\n", + "creating useless synapses, $w_{min}$ can be set to restrict the creation\n", + "of synapses to the cases where the value of the weight would be superior\n", + "to $w_{min}$. Default is $0.01 w_{max}$.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:15.602031Z", + "start_time": "2021-03-25T03:03:15.587416Z" + } + }, "outputs": [], - "source": [] + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "def show_weight(pre_ids, post_ids, weights, geometry, neu_id):\n", + " height, width = geometry\n", + " ids = np.where(pre_ids == neu_id)[0]\n", + " post_ids = post_ids[ids]\n", + " weights = weights[ids]\n", + "\n", + " X, Y = np.arange(height), np.arange(width)\n", + " X, Y = np.meshgrid(X, Y)\n", + " Z = np.zeros(geometry)\n", + " for id_, weight in zip(post_ids, weights):\n", + " h, w = id_ // width, id_ % width\n", + " Z[h, w] = weight\n", + "\n", + " fig = plt.figure()\n", + " ax = fig.gca(projection='3d')\n", + " surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, linewidth=0, antialiased=False)\n", + " fig.colorbar(surf, shrink=0.5, aspect=5)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:18.031905Z", + "start_time": "2021-03-25T03:03:15.606977Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "gaussian_weight = bp.connect.GaussianWeight(\n", + " sigma=0.1, w_max=1., w_min=0.01,\n", + " normalize=True, include_self=True)\n", + "pre_geom = post_geom = (40, 40)\n", + "gaussian_weight(pre_geom, post_geom)\n", + "\n", + "pre_ids = gaussian_weight.pre_ids\n", + "post_ids = gaussian_weight.post_ids\n", + "weights = gaussian_weight.weights\n", + "show_weight(pre_ids, post_ids, weights, pre_geom, 820)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### brainpy.connect.DOG\n", + "\n", + "\n", + "Builds a Difference-Of-Gaussian (dog) connection pattern between the two populations.\n", + "\n", + "Mathematically,\n", + "\n", + "$$\n", + "w(x, y) = w_{max}^+ \\cdot \\exp(-\\frac{(x-x_c)^2+(y-y_c)^2}{2\\sigma_+^2})\n", + " - w_{max}^- \\cdot \\exp(-\\frac{(x-x_c)^2+(y-y_c)^2}{2\\sigma_-^2})\n", + "$$\n", + "\n", + "where weights smaller than $0.01 * abs(w_{max} - w_{min})$ are not created and\n", + "self-connections are avoided by default (parameter allow_self_connections).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-25T03:03:20.846295Z", + "start_time": "2021-03-25T03:03:18.032908Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dog = bp.connect.DOG(\n", + " sigmas=(0.08, 0.15),\n", + " ws_max=(1.0, 0.7), w_min=0.01,\n", + " normalize=True, include_self=True)\n", + "h = 40\n", + "pre_geom = post_geom = (h, h)\n", + "dog(pre_geom, post_geom)\n", + "\n", + "pre_ids = dog.pre_ids\n", + "post_ids = dog.post_ids\n", + "weights = dog.weights\n", + "show_weight(pre_ids, post_ids, weights, (h, h), h * h // 2 + h // 2)" + ] } ], "metadata": { @@ -62,14 +1059,22 @@ }, "toc": { "base_numbering": 1, - "nav_menu": {}, + "nav_menu": { + "height": "411px", + "width": "316px" + }, "number_sections": false, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, - "toc_position": {}, + "toc_position": { + "height": "calc(100% - 180px)", + "left": "10px", + "top": "150px", + "width": "243.07px" + }, "toc_section_display": true, "toc_window_display": false }, @@ -104,5 +1109,5 @@ } }, "nbformat": 4, - "nbformat_minor": 5 + "nbformat_minor": 4 } diff --git a/docs/tutorials/numerical_solvers.ipynb b/docs/tutorials/numerical_solvers.ipynb index 2a9d4e1c..142a7a61 100644 --- a/docs/tutorials/numerical_solvers.ipynb +++ b/docs/tutorials/numerical_solvers.ipynb @@ -22,16 +22,16 @@ "id": "specialized-wyoming", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:11:10.299805Z", - "start_time": "2021-03-23T15:11:08.620690Z" + "end_time": "2021-03-24T11:08:28.570200Z", + "start_time": "2021-03-24T11:08:26.556045Z" } }, "outputs": [], "source": [ - "import sys\n", - "sys.path.append('../../')\n", + "import brainpy as bp\n", "\n", - "import brainpy as bp" + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.mplot3d import Axes3D" ] }, { @@ -47,7 +47,7 @@ "id": "complicated-italy", "metadata": {}, "source": [ - "### How to define an ODE function?" + "### How to define ODE functions?" ] }, { @@ -67,12 +67,12 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "failing-headset", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T14:10:54.574024Z", - "start_time": "2021-03-23T14:10:54.545736Z" + "end_time": "2021-03-24T11:08:28.586248Z", + "start_time": "2021-03-24T11:08:28.573214Z" } }, "outputs": [], @@ -95,12 +95,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "historical-chapel", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:30:04.376593Z", - "start_time": "2021-03-23T15:30:04.368594Z" + "end_time": "2021-03-24T11:08:28.602248Z", + "start_time": "2021-03-24T11:08:28.591212Z" } }, "outputs": [], @@ -132,12 +132,12 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "apparent-structure", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:07:37.736843Z", - "start_time": "2021-03-23T15:07:37.716840Z" + "end_time": "2021-03-24T11:08:28.617260Z", + "start_time": "2021-03-24T11:08:28.605262Z" } }, "outputs": [], @@ -164,12 +164,12 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "id": "artificial-curtis", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:11:22.057190Z", - "start_time": "2021-03-23T15:11:22.047195Z" + "end_time": "2021-03-24T11:08:28.633269Z", + "start_time": "2021-03-24T11:08:28.621269Z" } }, "outputs": [ @@ -196,7 +196,7 @@ " 'ssprk3']" ] }, - "execution_count": 3, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -215,12 +215,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "bronze-sport", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:13:54.738449Z", - "start_time": "2021-03-23T15:13:54.729900Z" + "end_time": "2021-03-24T11:08:28.649299Z", + "start_time": "2021-03-24T11:08:28.636270Z" } }, "outputs": [], @@ -245,7 +245,7 @@ "id": "christian-receipt", "metadata": {}, "source": [ - "Here, let's take the well known [FitzHugh–Nagumo model](https://en.wikipedia.org/wiki/FitzHugh%E2%80%93Nagumo_model) as an exmaple to illustrate how to define ODE solvers for brain modeling. The FitzHugh–Nagumo model (FHN) model has two dynamical variables, which are governed by the following equations:\n", + "Now, let's take the well known [FitzHugh–Nagumo model](https://en.wikipedia.org/wiki/FitzHugh%E2%80%93Nagumo_model) as an exmaple to illustrate how to define ODE solvers for brain modeling. The FitzHugh–Nagumo model (FHN) model has two dynamical variables, which are governed by the following equations:\n", "\n", "$$\n", "\\begin{align}\n", @@ -259,12 +259,12 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "id": "saved-participation", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:29:53.081577Z", - "start_time": "2021-03-23T15:29:53.073575Z" + "end_time": "2021-03-24T11:08:28.665257Z", + "start_time": "2021-03-24T11:08:28.653261Z" } }, "outputs": [], @@ -286,17 +286,17 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "annual-wrestling", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:37:54.696961Z", - "start_time": "2021-03-23T15:37:54.678955Z" + "end_time": "2021-03-24T11:08:28.681259Z", + "start_time": "2021-03-24T11:08:28.670264Z" } }, "outputs": [], "source": [ - "dt = 0.01; a=0.7; b=0.8; tau=12.5; Iext=1." + "a=0.7; b=0.8; tau=12.5; Iext=1." ] }, { @@ -309,28 +309,28 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "dated-sunset", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:37:57.984245Z", - "start_time": "2021-03-23T15:37:57.736440Z" + "end_time": "2021-03-24T11:08:28.952277Z", + "start_time": "2021-03-24T11:08:28.686261Z" } }, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 10, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -342,14 +342,13 @@ } ], "source": [ - "hist_times = np.arange(0, 100, 0.1)\n", + "hist_times = np.arange(0, 100, integral.dt)\n", "hist_V = []\n", "V, w = 0., 0.\n", "for t in hist_times:\n", " V, w = integral(V, w, t, Iext, a, b, tau)\n", " hist_V.append(V)\n", "\n", - "import matplotlib.pyplot as plt\n", "plt.plot(hist_times, hist_V)" ] }, @@ -382,12 +381,12 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "sexual-butler", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:42:17.921752Z", - "start_time": "2021-03-23T15:42:17.896456Z" + "end_time": "2021-03-24T11:08:28.968259Z", + "start_time": "2021-03-24T11:08:28.955260Z" } }, "outputs": [], @@ -424,12 +423,12 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 11, "id": "worthy-restriction", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:48:24.927865Z", - "start_time": "2021-03-23T15:48:24.919865Z" + "end_time": "2021-03-24T11:08:28.984261Z", + "start_time": "2021-03-24T11:08:28.971267Z" } }, "outputs": [], @@ -440,28 +439,28 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 12, "id": "regular-kernel", "metadata": { "ExecuteTime": { - "end_time": "2021-03-23T15:48:27.412192Z", - "start_time": "2021-03-23T15:48:27.019017Z" + "end_time": "2021-03-24T11:08:30.645360Z", + "start_time": "2021-03-24T11:08:28.987259Z" } }, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 17, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -473,11 +472,12 @@ } ], "source": [ - "hist_times = np.arange(0, 100, 0.1)\n", + "hist_times = np.arange(0, 100, integral.dt)\n", "hist_V, hist_m, hist_h, hist_n = [], [], [], []\n", "V, m, h, n = 0., 0., 0., 0.\n", "for t in hist_times:\n", - " V, m, h, n = integral(V, m, h, n, t, Iext, gNa, ENa, gK, EK, gL, EL, C)\n", + " V, m, h, n = integral(V, m, h, n, t, Iext, \n", + " gNa, ENa, gK, EK, gL, EL, C)\n", " hist_V.append(V)\n", " hist_m.append(m)\n", " hist_h.append(h)\n", @@ -507,13 +507,80 @@ "id": "incoming-result", "metadata": {}, "source": [ - "### How to define a SDE function?" + "### How to define SDE functions?" + ] + }, + { + "cell_type": "markdown", + "id": "parental-rogers", + "metadata": {}, + "source": [ + "For a one-dimensional stochastic differentiable equation (SDE) with scalar Wiener noise, it is given by\n", + "\n", + "$$\n", + "\\begin{align}\n", + "d X_{t}&=f\\left(X_{t}, t, p_1\\right) d t+g\\left(X_{t}, t, p_2\\right) d W_{t} \\quad (1)\n", + "\\end{align}\n", + "$$\n", + "\n", + "where $X_t = X(t)$ is the realization of a stochastic process or random variable, $f(X_t, t)$ is the drift coefficient, $g(X_t, t)$ denotes the diffusion coefficient, the stochastic process $W_t$ is called Wiener process. " + ] + }, + { + "cell_type": "markdown", + "id": "marine-pencil", + "metadata": {}, + "source": [ + "For this SDE system, we can define two Python funtions $f$ and $g$ to represent it." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "skilled-continuity", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:08:30.661294Z", + "start_time": "2021-03-24T11:08:30.648269Z" + } + }, + "outputs": [], + "source": [ + "def g_part(x, t, p1, p2):\n", + " dg = g(x, t, p2)\n", + " return dg\n", + "\n", + "def f_part(x, t, p1, p2):\n", + " df = f(x, t, p1)\n", + " return df" + ] + }, + { + "cell_type": "markdown", + "id": "worth-vertex", + "metadata": {}, + "source": [ + "Same with the ODE functions, the arguments before $t$ denotes the random variables, while the arguments defined after $t$ represents the parameters. For the SDE function with scalar noise, the size of the return data $dg$ and $df$ should be the same. For example, $df \\in R^d, dg \\in R^d$." + ] + }, + { + "cell_type": "markdown", + "id": "collect-connecticut", + "metadata": {}, + "source": [ + "However, for a more general SDE system, it usually has multi-dimensional driving Wiener process:\n", + "\n", + "$$\n", + "dX_t=f(X_t)dt+\\sum_{\\alpha=1}^{m}g_{\\alpha }(X_t)dW_t ^{\\alpha}\n", + "$$\n", + "\n", + "For such $m$-dimensional noise system, the coding schema is the same with the scalar ones, but with the difference of that the data size of $dg$ has one more dimension. For example, $df \\in R^{d}, dg \\in R^{d \\times m}$." ] }, { "cell_type": "code", "execution_count": null, - "id": "stopped-finding", + "id": "drawn-volleyball", "metadata": {}, "outputs": [], "source": [] @@ -521,18 +588,418 @@ { "cell_type": "code", "execution_count": null, - "id": "competitive-attack", + "id": "entire-harrison", "metadata": {}, "outputs": [], "source": [] }, + { + "cell_type": "markdown", + "id": "human-virgin", + "metadata": {}, + "source": [ + "### How to define the numerical integration for SDEs?" + ] + }, + { + "cell_type": "markdown", + "id": "numeric-success", + "metadata": {}, + "source": [ + "Brefore the numerical integration of SDE functions, we should distinguish two kinds of SDE integrals. For the integration of system (1), we can get\n", + "\n", + "$$\n", + "\\begin{align}\n", + "X_{t}&=X_{t_{0}}+\\int_{t_{0}}^{t} f\\left(X_{s}, s\\right) d s+\\int_{t_{0}}^{t} g\\left(X_{s}, s\\right) d W_{s} \\quad (2)\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "neutral-telescope", + "metadata": {}, + "source": [ + "In 1940s, the Japanese mathematician K. Ito denoted a type of integral called *Ito stochastic integral*. In 1960s, the Russian physicist R. L. Stratonovich proposed an other kind of stochastic integral called *Stratonovich stochastic integral* and used the symbol \"$\\circ$\" to distinct it from the former Ito integral.\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "d X_{t} &=f\\left(X_{t}, t\\right) d t+g\\left(X_{t}, t\\right) \\circ d W_{t} \\\\\n", + "X_{t} &=X_{t_{0}}+\\int_{t_{0}}^{t} f\\left(X_{s}, s\\right) d s+\\int_{t_{0}}^{t} g\\left(X_{s}, s\\right) \\circ d W_{s} \\quad (3)\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "classical-authority", + "metadata": {}, + "source": [ + "The difference of Ito integral (2) and Stratonovich integral (3) lies at the second integral term, which can be written in a general form as\n", + "\n", + "$$\n", + "\\begin{align}\n", + "\\int_{t_{0}}^{t} g\\left(X_{s}, s\\right) d W_{s} &=\\lim _{h \\rightarrow 0} \\sum_{k=0}^{m-1} g\\left(X_{\\tau_{k}}, \\tau_{k}\\right)\\left(W\\left(t_{k+1}\\right)-W\\left(t_{k}\\right)\\right) \\\\\n", + "\\mathrm{where} \\quad h &= t_{k+1} - t_{k} \\\\\n", + "\\tau_k &= (1-\\lambda)t_k +\\lambda t_{k+1} \n", + "\\end{align}\n", + "$$\n", + "\n", + "- In the stochastic integral of the Ito SDE, $\\lambda=0$, thus $\\tau_k=t_k$; \n", + "\n", + "- In the definition of the Stratonovich integral, $\\lambda=0.5$, thus $\\tau_k=(t_{k+1} + t_{k}) / 2$." + ] + }, + { + "cell_type": "markdown", + "id": "japanese-chart", + "metadata": {}, + "source": [ + "In BrainPy, these two different integrals can be easily implemented. What need the users do is to provide a keyword `sde_type` in decorator `bp.sdeint`. `sde_type` can be \"bp.STRA_SDE\" or \"bp.ITO_SDE\" (default). Also, the different type of Wiener process can also be easily distinguished by the `wiener_type` keyword. It can be \"bp.SCALAR_WIENER\" (default) or \"bp.VECTOR_WIENER\"." + ] + }, + { + "cell_type": "markdown", + "id": "legislative-geography", + "metadata": {}, + "source": [ + "Now, let's numerically integrate the SDE (1) by the Ito way with the Milstein method:" + ] + }, { "cell_type": "code", - "execution_count": null, - "id": "short-explanation", + "execution_count": 14, + "id": "beginning-buying", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:08:30.677270Z", + "start_time": "2021-03-24T11:08:30.664263Z" + } + }, + "outputs": [], + "source": [ + "def g_part(x, t, p1, p2):\n", + " dg = g(x, t, p2)\n", + " return dg # shape=(d,)\n", + "\n", + "@bp.sdeint(g=g_part, method='milstein')\n", + "def f_part(x, t, p1, p2):\n", + " df = f(x, t, p1)\n", + " return df # shape=(d,)" + ] + }, + { + "cell_type": "markdown", + "id": "minimal-asthma", "metadata": {}, + "source": [ + "Or, it can be expressed as:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "casual-architecture", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:08:30.708260Z", + "start_time": "2021-03-24T11:08:30.680263Z" + } + }, "outputs": [], - "source": [] + "source": [ + "def g_part(x, t, p1, p2):\n", + " dg = g(x, t, p2)\n", + " return dg # shape=(d,)\n", + "\n", + "def f_part(x, t, p1, p2):\n", + " df = f(x, t, p1)\n", + " return df # shape=(d,)\n", + "\n", + "integral = bp.sdeint(f=f_part, g=g_part, method='milstein')" + ] + }, + { + "cell_type": "markdown", + "id": "willing-clear", + "metadata": {}, + "source": [ + "However, if you try to numerically integrate the SDE with multi-dimensional Wiener process by the Stratonovich ways, you can code it like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ready-conspiracy", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:08:30.739259Z", + "start_time": "2021-03-24T11:08:30.712276Z" + } + }, + "outputs": [], + "source": [ + "def g_part(x, t, p1, p2):\n", + " dg = g(x, t, p2)\n", + " return dg # shape=(d, m)\n", + "\n", + "def f_part(x, t, p1, p2):\n", + " df = f(x, t, p1)\n", + " return df # shape=(d,)\n", + "\n", + "integral = bp.sdeint(f=f_part, g=g_part, method='milstein', \n", + " sde_type=bp.STRA_SDE, \n", + " wiener_type=bp.SCALAR_WIENER)" + ] + }, + { + "cell_type": "markdown", + "id": "challenging-rental", + "metadata": {}, + "source": [ + "### Example 3: Noisy Lorenz system" + ] + }, + { + "cell_type": "markdown", + "id": "stretch-heaven", + "metadata": {}, + "source": [ + "Here, let's demenstrate how to define a numerical solver for SDEs with the famous [Lorenz system](https://en.wikipedia.org/wiki/Lorenz_system): \n", + "\n", + "$$\n", + "\\begin{array}{l}\n", + "\\frac{d x}{dt}&=\\sigma(y-x) &+ px*\\xi_x \\\\\n", + "\\frac{d y}{dt}&=x(\\rho-z)-y &+ py*\\xi_y\\\\\n", + "\\frac{d z}{dt}&=x y-\\beta z &+ pz*\\xi_z\n", + "\\end{array}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "checked-greece", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:08:30.755264Z", + "start_time": "2021-03-24T11:08:30.742270Z" + } + }, + "outputs": [], + "source": [ + "sigma = 10\n", + "beta = 8 / 3\n", + "rho = 28\n", + "p = 0.1\n", + "\n", + "def lorenz_g(x, y, z, t):\n", + " return p * x, p * y, p * z\n", + "\n", + "def lorenz_f(x, y, z, t):\n", + " dx = sigma * (y - x)\n", + " dy = x * (rho - z) - y\n", + " dz = x * y - beta * z\n", + " return dx, dy, dz\n", + "\n", + "lorenz = bp.sdeint(f=lorenz_f, g=lorenz_g, sde_type=bp.ITO_SDE, wiener_type=bp.SCALAR_WIENER, dt=0.005)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "thick-threat", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T11:08:39.075036Z", + "start_time": "2021-03-24T11:08:38.687662Z" + }, + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'z')" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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JtwRZjQayc3NtbqVTDaG1tJtJ5WOFALX1NTQ09MzrJow3dDr9VZJaU9Pj9UUYzStfktLC5dddhlPPPEEy5Ytm/JzdIXlJyDMZjO7d+9mw4YNfPXVV6SkpLBmzRrOPvtsp7PYUu9zfHy8U6YGR4qhIfDQwQiiKFJVVYVGoyE9Pf2oEluCZPSYkZFhbXBxJIUFiynDJZdcwmOPPcZZZ501Lec3lNwTlJSOBy5yTyfMZjMHDx5k/fr1fPHFF8THx1t70keSZkq2wzNnzjxqhg+23WCSpZWbmxtz5sw56tbDgFW7npmZaXfDHCqFrampoa+vj3feeYeHHnqI8847b1rO76qrruL777+no6OD8PBwHnjgAVavXj0RSel44CL30YLUZy31pEdERLBq1SouuOACa71TumilrrejDVEUKSwsZGBgAJlMhtlstu51j5Z76kjEHgpRFNm+fTt///vfUalUZGRkcN99901ZdvwYgIvcxwJEUaS4uJj169ezdetWAgICmDdvHocOHeLtt98+JjqppHOUy+XWLL20121ra0Ov10/qBBRn4CyxwZJBv+SSS7jzzju55JJLKC0txcfH56gaVk4xjn1y33333Xz66acolUpmzJjB66+/bm3SeOSRR3j11VeRy+U8/fTTnHPOORM/62MEoijy+uuvc99995GUlIRCoWDlypWsXLlyynvSR4LUq+7h4THimGPbCSgajWZSVWaOIPWHO9NG2t/fz6WXXsott9zCVVddNenncoxiSi6USc2urFixgoKCAvLy8khJSeGRRx4BoKioiA8++IDCwkK2bdvGrbfeajes4HiFIAhUVVVx6NAhfvjhB1577TVMJhPXXXcd5513Hs899xyNjY1OjzE+UkjbBy8vr1HndikUCiIiIsjMzGTRokUEBgbS2NjI7t27KSkpQaVSjTrjbTwYD7HVajVXXnklN91005QTe9u2baSmppKcnMyjjz46pe91tDBlYfmmTZtYv3497777rpXkf/3rXwE455xzuP/++zn55JMnevhjGrY96Rs3bkSv11tdZuLj46dkdTSbzeTl5REYGEh8fPyEj2Gb1BrPqCNHGA+xBwYGuPLKK7niiiu46aabJnT+zsJkMpGSksJXX31FTEwMixYt4v333z+ann5TsnJPmQbztdde44orrgAsDRMnnXSS9bGYmBgaGxun6q2POgRBIDo6mttvv53f//73tLa2smnTJu644w56e3u54IILWLVq1aRNxZT816T5ahOFTCYjODiY4OBgu1FHFRUV45KTwmEPNmemoup0On7xi1+wZs0afvWrX034/J3F3r17SU5OJikpCYArr7ySzZs3HxOGnZOJcZP7rLPOoqWlZdjP//nPf7Jq1SrrvxUKBddccw2Aw7D0WCjLTAcEQSAiIoJbbrmFW265hY6ODj7++GPWrl1LR0cH5513HitXrpzwwHpJMBMZGUlUVNSknndAQAABAQF2tfTa2lqUSiXh4eGEhIQ47CbTaDTk5uY6ZUSh1+u57rrrOPfcc7nlllum5bpobGy0uwnGxMSwZ8+eKX/f6ca4yf3111+P+vibb77Jli1b+Oabb6xfVExMDPX19dbnNDQ0TOqFeDwhJCSEm266iZtuuomuri4++eQTHnroIerr66096UMHEYwEaQpIbGwsERERU3bOgiDg6+uLr68vM2bMsBpQ5OTk2M008/DwYGBgwGquOBaxDQYDN954I6eddhq33377tN3wfy6LzaTuubdt28add97JDz/8YGdzU1hYyNVXX83evXtpampi+fLllJeXHxM91scKent72bJlCxs3bqSiooLly5ezevVq5s2b55DokhIuISFhyl0+R4M000wyctDpdKSlpY15TkajkZtvvpk5c+Zw7733Tiu5du3axf33388XX3wBMCwndBRw7JfCkpOTrf3IACeddBIvvvgiYAnVX3vtNRQKBf/973+nTXF0PEKtVvP555+zfv16ioqKWLZsGatXr2bRokXI5XI0Go111PGxMNMMLAmxQ4cOERYWRl9fn7WWHh4ePsyxxWQyceutt5KYmMgDDzww7aum0WgkJSWFb775hujoaBYtWsR77703qaYP48SxT+7pwrp167j//vspLi5m7969LFy40PrYiVZP12q1fPnll6xbt45Dhw6xcOFC9u7dy0cffWRNCB1tSJNJZs2aZVXnDXVsCQ4OxsPDg4iICO68805CQkJ45JFHjprW/bPPPuMPf/gDJpOJG2+8kXvvvfeonMcgXOSWUFxcjEwm4ze/+Q2PP/64ldxFRUVcddVV1vD/rLPOoqys7IQJ/8vLyznvvPOYM2cO5eXlnHzyyaxatYpTTz31qDiUwmFij+YqK1kRv/DCC7z77ruEhITwxBNPcPrp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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "hist_times = np.arange(0, 50, lorenz.dt)\n", + "hist_x, hist_y, hist_z = [], [], []\n", + "x, y, z = 1., 1., 1.\n", + "for t in hist_times:\n", + " x, y, z = lorenz(x, y, z, t)\n", + " hist_x.append(x)\n", + " hist_y.append(y)\n", + " hist_z.append(z)\n", + "\n", + "fig = plt.figure()\n", + "ax = plt.axes(projection='3d')\n", + "ax.plot3D(hist_x, hist_y, hist_z)\n", + "ax.set_xlabel('x')\n", + "ax.set_xlabel('y')\n", + "ax.set_xlabel('z')" + ] + }, + { + "cell_type": "markdown", + "id": "industrial-specific", + "metadata": {}, + "source": [ + "## Backend-independent Property" + ] + }, + { + "cell_type": "markdown", + "id": "theoretical-beach", + "metadata": {}, + "source": [ + "Actually, BrainPy provides general numerical solvers for user-defined differential equations. It is backend-independent. Users can define their differential equations with any computation backend they prefer, such as NumPy, PyTorch, TensorFlow, Jax. The only thing need to do is to provide the necessary operations to `brainpy.backend`. For the needed operations, you can inspect them by " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "loved-motion", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T12:27:00.911802Z", + "start_time": "2021-03-24T12:27:00.865495Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['normal',\n", + " 'exp',\n", + " 'matmul',\n", + " 'sum',\n", + " 'as_tensor',\n", + " 'zeros',\n", + " 'ones',\n", + " 'arange',\n", + " 'eye',\n", + " 'vstack',\n", + " 'reshape',\n", + " 'shape']" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bp.backend.NEEDED_OPS" + ] + }, + { + "cell_type": "markdown", + "id": "divine-pakistan", + "metadata": {}, + "source": [ + "After you define the necessary functions, you need to register them by `bp.backend.set_ops(**kwargs)`. Or, you can put all these functions into a \".py\" file, then import this python script as a module and set `bp.backend.set_ops_from_module(module)`." + ] + }, + { + "cell_type": "markdown", + "id": "local-development", + "metadata": {}, + "source": [ + "Currently, BrainPy inherently supports NumPy, Numba, PyTorch, TensorFlow. You can easily switch backend just by typing `bp.backend.set()`. For example," + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "protecting-stations", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T12:36:39.156532Z", + "start_time": "2021-03-24T12:36:39.140568Z" + } + }, + "outputs": [], + "source": [ + "bp.backend.set('numpy')" + ] + }, + { + "cell_type": "markdown", + "id": "rolled-shopper", + "metadata": {}, + "source": [ + "switch the backend to NumPy." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "natural-treaty", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T12:36:40.646506Z", + "start_time": "2021-03-24T12:36:40.528169Z" + } + }, + "outputs": [], + "source": [ + "bp.backend.set('numba')" + ] + }, + { + "cell_type": "markdown", + "id": "choice-circulation", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T12:36:42.284988Z", + "start_time": "2021-03-24T12:36:42.258984Z" + } + }, + "source": [ + "switch the backend to Numba." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "sustained-builder", + "metadata": { + "ExecuteTime": { + "end_time": "2021-03-24T12:36:51.150318Z", + "start_time": "2021-03-24T12:36:49.517904Z" + } + }, + "outputs": [], + "source": [ + "bp.backend.set('pytorch')" + ] + }, + { + "cell_type": "markdown", + "id": "advisory-upper", + "metadata": {}, + "source": [ + "switch the backend to pytorch." + ] } ], "metadata": { @@ -557,8 +1024,8 @@ "toc": { "base_numbering": 1, "nav_menu": { - "height": "198px", - "width": "397px" + "height": "265px", + "width": "436px" }, "number_sections": false, "sideBar": true, diff --git a/docs/tutorials/quick_start.ipynb b/docs/tutorials/quick_start.ipynb index d05c5e74..1f62410c 100644 --- a/docs/tutorials/quick_start.ipynb +++ b/docs/tutorials/quick_start.ipynb @@ -858,7 +858,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.9" }, "toc": { "base_numbering": 1, diff --git a/examples/networks/Wu_2008_CANN.py b/examples/networks/Wu_2008_CANN.py new file mode 100644 index 00000000..2fe151c3 --- /dev/null +++ b/examples/networks/Wu_2008_CANN.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- + + +import numpy as np + +import brainpy as bp + +bp.backend.set(dt=0.05) +bp.integrators.set_default_odeint('rk4') + + +class CANN1D(bp.NeuGroup): + target_backend = ['numpy', 'numba'] + + def __init__(self, num, tau=1., k=8.1, a=0.5, A=10., J0=4., + z_min=-np.pi, z_max=np.pi, **kwargs): + # parameters + self.tau = tau # The synaptic time constant + self.k = k # Degree of the rescaled inhibition + self.a = a # Half-width of the range of excitatory connections + self.A = A # Magnitude of the external input + self.J0 = J0 # maximum connection value + + # feature space + self.z_min = z_min + self.z_max = z_max + self.z_range = z_max - z_min + self.x = np.linspace(z_min, z_max, num) # The encoded feature values + + # variables + self.u = np.zeros(num) + self.input = np.zeros(num) + + # The connection matrix + self.conn_mat = self.make_conn(self.x) + + super(CANN1D, self).__init__(size=num, **kwargs) + + self.rho = num / self.z_range # The neural density + self.dx = self.z_range / num # The stimulus density + + def dist(self, d): + d = np.remainder(d, self.z_range) + d = np.where(d > 0.5 * self.z_range, d - self.z_range, d) + return d + + def make_conn(self, x): + assert np.ndim(x) == 1 + x_left = np.reshape(x, (-1, 1)) + x_right = np.repeat(x.reshape((1, -1)), len(x), axis=0) + d = self.dist(x_left - x_right) + Jxx = self.J0 * np.exp(-0.5 * np.square(d / self.a)) / (np.sqrt(2 * np.pi) * self.a) + return Jxx + + def get_stimulus_by_pos(self, pos): + return self.A * np.exp(-0.25 * np.square(self.dist(self.x - pos) / self.a)) + + @staticmethod + @bp.odeint + def int_u(u, t, conn, k, tau, Iext): + r1 = np.square(u) + r2 = 1.0 + k * np.sum(r1) + r = r1 / r2 + Irec = np.dot(conn, r) + du = (-u + Irec + Iext) / tau + return du + + def update(self, _t): + self.u = self.int_u(self.u, _t, self.conn_mat, + self.k, self.tau, self.input) + self.input[:] = 0. + + +def task1_population_coding(): + cann = CANN1D(num=512, k=0.1, monitors=['u']) + + I1 = cann.get_stimulus_by_pos(0.) + Iext, duration = bp.inputs.constant_current([(0., 1.), (I1, 8.), (0., 8.)]) + cann.run(duration=duration, inputs=('input', Iext)) + + bp.visualize.animate_1D( + dynamical_vars=[{'ys': cann.mon.u, 'xs': cann.x, 'legend': 'u'}, + {'ys': Iext, 'xs': cann.x, 'legend': 'Iext'}], + frame_step=1, + frame_delay=100, + show=True, + # save_path='../../images/CANN-encoding.gif' + ) + + +def task2_template_matching(): + cann = CANN1D(num=512, k=8.1, monitors=['u']) + + dur1, dur2, dur3 = 10., 30., 0. + num1 = int(dur1 / bp.backend.get_dt()) + num2 = int(dur2 / bp.backend.get_dt()) + num3 = int(dur3 / bp.backend.get_dt()) + Iext = np.zeros((num1 + num2 + num3,) + cann.size) + Iext[:num1] = cann.get_stimulus_by_pos(0.5) + Iext[num1:num1 + num2] = cann.get_stimulus_by_pos(0.) + Iext[num1:num1 + num2] += 0.1 * cann.A * np.random.randn(num2, *cann.size) + cann.run(duration=dur1 + dur2 + dur3, inputs=('input', Iext)) + + bp.visualize.animate_1D( + dynamical_vars=[{'ys': cann.mon.u, 'xs': cann.x, 'legend': 'u'}, + {'ys': Iext, 'xs': cann.x, 'legend': 'Iext'}], + frame_step=5, + frame_delay=50, + show=True, + # save_path='../../images/CANN-decoding.gif' + ) + + +def task3_smooth_tracking(): + cann = CANN1D(num=512, k=8.1, monitors=['u']) + + dur1, dur2, dur3 = 20., 20., 20. + num1 = int(dur1 / bp.backend.get_dt()) + num2 = int(dur2 / bp.backend.get_dt()) + num3 = int(dur3 / bp.backend.get_dt()) + position = np.zeros(num1 + num2 + num3) + position[num1: num1 + num2] = np.linspace(0., 12., num2) + position[num1 + num2:] = 12. + position = position.reshape((-1, 1)) + Iext = cann.get_stimulus_by_pos(position) + cann.run(duration=dur1 + dur2 + dur3, inputs=('input', Iext)) + + bp.visualize.animate_1D( + dynamical_vars=[{'ys': cann.mon.u, 'xs': cann.x, 'legend': 'u'}, + {'ys': Iext, 'xs': cann.x, 'legend': 'Iext'}], + frame_step=5, + frame_delay=50, + show=True, + # save_path='../../images/CANN-tracking.gif' + ) + + +task3_smooth_tracking() diff --git a/examples/neurons/HindmarshRose_model.py b/examples/neurons/HindmarshRose_model.py new file mode 100644 index 00000000..64f10850 --- /dev/null +++ b/examples/neurons/HindmarshRose_model.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +import brainpy as bp +bp.backend.set('numba') + + +class HRNeuron(bp.NeuGroup): + target_backend = ['numpy', 'numba'] + + def __init__(self, num, a=1., b=3., c=1., d=5., s=4., x_r=-1.6, r=0.001, Vth=1.9, **kwargs): + + def dev_hr(x, y, z, t, Isyn): + dx = y - a * x ** 3 + b * x * x - z + Isyn + dy = c - d * x * x - y + dz = r * (s * (x - x_r) - z) + return dx, dy, dz + + self.int_hr = bp.odeint(f=dev_hr, method='rk4', dt=0.02) + + super(HRNeuron, self).__init__(size=num, **kwargs) + + def update(self, _t): + pass + +hr = HRNeuron(1) + +analyzer = bp.analysis.FastSlowBifurcation( + integrals=hr.int_hr, + fast_vars={'x': [-3, 3], 'y': [-10., 5.]}, + slow_vars={'z': [-5., 5.]}, + pars_update={'Isyn': 0.5}, + numerical_resolution=0.001 +) +analyzer.plot_bifurcation() +analyzer.plot_trajectory([{'x': 1., 'y': 0., 'z': -0.0}], + duration=300., + show=True) + diff --git a/examples/synapses/AMPA_synapse.py b/examples/synapses/AMPA_synapse.py index 9e5edb34..b056cbc8 100644 --- a/examples/synapses/AMPA_synapse.py +++ b/examples/synapses/AMPA_synapse.py @@ -106,7 +106,7 @@ class AMPA1_vec(bp.TwoEndConn): class AMPA1_mat(bp.TwoEndConn): - target_backend = ['numpy', 'numba', 'numba-parallel'] + target_backend = ['numpy', 'numba'] def __init__(self, pre, post, conn, delay=0., g_max=0.10, E=0., tau=2.0, **kwargs): # parameters @@ -120,7 +120,7 @@ class AMPA1_mat(bp.TwoEndConn): self.conn_mat = conn.requires('conn_mat') self.size = bp.backend.shape(self.conn_mat) - # data + # variables self.s = bp.backend.zeros(self.size) self.g = self.register_constant_delay('g', size=self.size, delay_time=delay) @@ -137,7 +137,7 @@ class AMPA1_mat(bp.TwoEndConn): if self.pre.spike[i] > 0: self.s[i] += self.conn_mat[i] self.g.push(self.g_max * self.s) - g=self.g.pull() + g = self.g.pull() self.post.input -= bp.backend.sum(g, axis=0) * (self.post.V - self.E) -- 2.34.1 From 0d3e2fa300cf895d7d70b0f0f29f9b4599b680ba Mon Sep 17 00:00:00 2001 From: Chaoming Wang Date: Thu, 25 Mar 2021 11:11:54 +0800 Subject: [PATCH 15/15] version update --- develop/conda-recipe/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/develop/conda-recipe/meta.yaml b/develop/conda-recipe/meta.yaml index 078f4c78..f9aafcd2 100644 --- a/develop/conda-recipe/meta.yaml +++ b/develop/conda-recipe/meta.yaml @@ -1,6 +1,6 @@ package: name: brainpy-simulator - version: "0.3.5" + version: "1.0.0-alpha" source: path: ../../ -- 2.34.1