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 isinstance(var, ast.Name): raise errors.DiffEqError(self.return_error_msg) self.returns.append(var.id) else: raise errors.DiffEqError(self.return_error_msg) 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 _get_args(f): # 1. get the function arguments original_args = [] args = [] kwargs = [] for name, par in inspect.signature(f).parameters.items(): if par.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: args.append(par.name) elif par.kind is inspect.Parameter.VAR_POSITIONAL: args.append(par.name) elif par.kind is inspect.Parameter.KEYWORD_ONLY: args.append(par.name) elif par.kind is inspect.Parameter.POSITIONAL_ONLY: raise errors.BrainPyError( 'Don not support positional only parameters, e.g., /') elif par.kind is inspect.Parameter.VAR_KEYWORD: kwargs.append(par.name) else: raise errors.BrainPyError(f'Unknown argument type: {par.kind}') original_args.append(str(par)) # 2. analyze the function arguments # 2.1 class keywords class_kw = [] if original_args[0] in CLASS_KEYWORDS: class_kw.append(original_args[0]) original_args = original_args[1:] args = args[1:] for a in original_args: if a.split('=')[0].strip() in CLASS_KEYWORDS: raise errors.DiffEqError(f'Class keywords "{a}" must be defined ' f'as the first argument.') return class_kw, args, kwargs, original_args
def _get_args(f): """Get the function arguments""" args = [] kwargs = {} for name, par in inspect.signature(f).parameters.items(): if par.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: if par.default is inspect._empty: args.append(par.name) else: kwargs[par.name] = par.default elif par.kind is inspect.Parameter.VAR_POSITIONAL: raise errors.DiffEqError( f'{JointEq.__name__} does not support VAR_POSITIONAL parameters ' f'*{par.name} (error in {f}).') elif par.kind is inspect.Parameter.KEYWORD_ONLY: raise errors.DiffEqError( f'{JointEq.__name__} does not support KEYWORD_ONLY parameters, ' f'e.g., * (error in {f}).') elif par.kind is inspect.Parameter.POSITIONAL_ONLY: raise errors.DiffEqError( f'{JointEq.__name__} does not support POSITIONAL_ONLY parameters, ' 'e.g., / (error in {f}).') elif par.kind is inspect.Parameter.VAR_KEYWORD: raise errors.DiffEqError( f'{JointEq.__name__} does not support VAR_KEYWORD ' f'arguments **{par.name} (error in {f}).') else: raise errors.DiffEqError(f'Unknown argument type: {par.kind}') # variables vars = [] for a in args: if a == 't': break vars.append(a) else: raise ValueError('Do not find time variable "t".') return vars, args, kwargs
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 len(arguments) > 0 and arguments[0] in backend.CLASS_KEYWORDS: class_kw = arguments[0] arguments = arguments[1:] for a in arguments: 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
def solve(self, diff_eq, var): if analysis_by_sympy is None or sympy is None: raise errors.PackageMissingError( f'Package "sympy" must be installed when the users ' f'want to utilize {ExponentialEuler.__name__}. ') f_expressions = diff_eq.get_f_expressions( substitute_vars=diff_eq.var_name) # code lines self.code_lines.extend( [f" {str(expr)}" for expr in f_expressions[:-1]]) # get the linear system using sympy f_res = f_expressions[-1] if len(f_res.code) > 500: raise errors.DiffEqError( f'Too complex differential equation:\n\n' f'{f_res.code}\n\n' f'SymPy cannot analyze. Please use {ExpEulerAuto} to ' f'make Exponential Euler integration due to it is capable of ' f'performing automatic differentiation.') df_expr = analysis_by_sympy.str2sympy(f_res.code).expr.expand() s_df = sympy.Symbol(f"{f_res.var_name}") self.code_lines.append( f' {s_df.name} = {analysis_by_sympy.sympy2str(df_expr)}') # 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.diff(df_expr, var, evaluate=True) # TODO: linear has unknown symbol self.code_lines.append( f' {s_linear.name} = {analysis_by_sympy.sympy2str(linear)}') # linear exponential self.code_lines.append( f' {s_linear_exp.name} = math.exp({s_linear.name} * {C.DT})') # df part df_part = (s_linear_exp - 1) / s_linear * s_df self.code_lines.append( f' {s_df_part.name} = {analysis_by_sympy.sympy2str(df_part)}') else: # df part self.code_lines.append( f' {s_df_part.name} = {s_df.name} * {C.DT}') return s_df_part
def call(t, *vars, **args_and_kwargs): params = dict(t=t) for var in f_vars: params[var] = vars[all_vars.index(var)] for par in f_args[len(f_vars) + 1:]: if par in args_and_kwargs: params[par] = args_and_kwargs[par] else: if par not in all_vars: raise errors.DiffEqError( f'Missing {par} during the functional call of {f}.') params[par] = vars[all_vars.index(par)] for par, value in f_kwargs.items(): if par in args_and_kwargs: params[par] = args_and_kwargs[par] return f(**params)
def _build_integrator(self, eq): if isinstance(eq, joint_eq.JointEq): results = [] for sub_eq in eq.eqs: results.extend(self._build_integrator(sub_eq)) return results else: vars, pars, _ = utils.get_args(eq) # checking if len(vars) != 1: raise errors.DiffEqError( f'{self.__class__} only supports numerical integration ' f'for one variable once, while we got {vars} in {eq}. ' f'Please split your multiple variables into multiple ' f'derivative functions.') # gradient function value_and_grad = math.vector_grad(eq, argnums=0, dyn_vars=self.dyn_var, return_value=True) # integration function def integral(*args, **kwargs): assert len(args) > 0 dt = kwargs.pop('dt', math.get_dt()) linear, derivative = value_and_grad(*args, **kwargs) phi = math.where(linear == 0., math.ones_like(linear), (math.exp(dt * linear) - 1) / (dt * linear)) return args[0] + dt * phi * derivative return [ (integral, vars, pars), ]
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 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_With(self, node): raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' f'analyze "with" block 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_While(self, node): raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support to ' f'analyze "while" loops in differential equation.')
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 backend.CLASS_KEYWORDS: class_kw.append(reduced_args[0]) reduced_args = reduced_args[1:] for a in reduced_args: 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 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
def visit_AnnAssign(self, node): raise errors.DiffEqError(f'Currently, {self.__class__.__name__} do not support an ' f'assignment with a type annotation.')
def __init__(self, eqs): # equations if not isinstance(eqs, (tuple, list)): raise errors.DiffEqError(f'"eqs" only supports list/tuple of ' f'derivative functions, but got {eqs}.') for eq in eqs: if not callable(eq): raise errors.DiffEqError( f'"eqs" only supports list/tuple of ' f'derivative functions, but got {eq}.') # variables in equations self.vars_in_eqs = [] vars_in_eqs = [] for eq in eqs: vars, _, _ = _get_args(eq) for var in vars: if var in vars_in_eqs: raise errors.DiffEqError( f'Variable "{var}" has been used, however we got a same ' f'variable name in {eq}. Please change another name.') vars_in_eqs.extend(vars) self.vars_in_eqs.append(vars) # arguments in equations self.args_in_eqs = [] all_arg_pars = [] all_kwarg_pars = dict() for eq in eqs: vars, args, kwargs = _get_args(eq) self.args_in_eqs.append(args + list(kwargs.keys())) for par in args[len(vars) + 1:]: if (par not in vars_in_eqs) and (par not in all_arg_pars) and ( par not in all_kwarg_pars): all_arg_pars.append(par) for key, value in kwargs.values(): if key in all_kwarg_pars and value != all_kwarg_pars[key]: raise errors.DiffEqError( f'We got two different default value of "{key}": ' f'{all_kwarg_pars[key]} != {value}') elif (key not in vars_in_eqs) and (key not in all_arg_pars): all_kwarg_pars[key] = value else: raise errors.DiffEqError # # variable names provided # if not isinstance(variables, (tuple, list)): # raise errors.DiffEqError(f'"variables" must be a list/tuple of str, but we got {variables}') # for v in variables: # if not isinstance(v, str): # raise errors.DiffEqError(f'"variables" must be a list/tuple of str, but we got {v} in "variables"') # if len(vars_in_eqs) != len(variables): # raise errors.DiffEqError(f'We detect {len(vars_in_eqs)} variables "{vars_in_eqs}" ' # f'in the provided equations. However, the used provided ' # f'"variables" have {len(variables)} variables ' # f'"{variables}".') # if len(set(vars_in_eqs) - set(variables)) != 0: # raise errors.DiffEqError(f'We detect there are variable "{vars_in_eqs}" in the provided ' # f'equations, while the user provided variables "{variables}" ' # f'is not the same.') # finally self.eqs = eqs # self.variables = variables self.arg_keys = vars_in_eqs + ['t'] + all_arg_pars self.kwarg_keys = list(all_kwarg_pars.keys()) self.kwargs = all_kwarg_pars parameters = [ inspect.Parameter(vp, inspect.Parameter.POSITIONAL_OR_KEYWORD) for vp in self.arg_keys ] parameters.extend([ inspect.Parameter(k, kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, default=all_kwarg_pars[k]) for k in self.kwarg_keys ]) signature = inspect.signature(eqs[0]) self.__signature__ = signature.replace(parameters=parameters) self.__name__ = 'joint_eq'
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: Do 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. """ # get the function arguments reduced_args = [] 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: raise errors.DiffEqError(f'In BrainPy, numerical integrators do not support KEYWORD_ONLY ' f'parameters, e.g., * (error in {f}).') elif par.kind is inspect.Parameter.POSITIONAL_ONLY: raise errors.DiffEqError(f'In BrainPy, numerical integrators do not support POSITIONAL_ONLY ' f'parameters, e.g., / (error in {f}).') elif par.kind is inspect.Parameter.VAR_KEYWORD: # TODO raise errors.DiffEqError(f'In BrainPy, numerical integrators do not support VAR_KEYWORD ' f'arguments: {str(par)} (error in {f}).') else: raise errors.DiffEqError(f'Unknown argument type: {par.kind} (error in {f}).') args.append(str(par)) # variable names vars = [] for a in reduced_args: if a == 't': break vars.append(a) else: raise ValueError('Do not find time variable "t".') pars = reduced_args[len(vars):] return vars, pars, args
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 build(self): if analysis_by_sympy is None or sympy is None: raise errors.PackageMissingError( f'Package "sympy" must be installed when the users ' f'want to utilize {ExponentialEuler.__name__}. ') # check bound method if hasattr(self.f, '__self__'): self.code_lines = [ f'def {self.func_name}({", ".join(["self"] + list(self.arguments))}):' ] # code scope closure_vars = inspect.getclosurevars(self.f) self.code_scope.update(closure_vars.nonlocals) self.code_scope.update(dict(closure_vars.globals)) self.code_scope['math'] = math analysis = separate_variables(self.f) variables_for_returns = analysis['variables_for_returns'] expressions_for_returns = analysis['expressions_for_returns'] for vi, (key, all_var) in enumerate(variables_for_returns.items()): # separate variables sd_variables = [] for v in all_var: if len(v) > 1: raise ValueError( f'Cannot analyze multi-assignment code line: {v}.') sd_variables.append(v[0]) expressions = expressions_for_returns[key] var_name = self.variables[vi] diff_eq = analysis_by_sympy.SingleDiffEq(var_name=var_name, variables=sd_variables, expressions=expressions, derivative_expr=key, scope=self.code_scope, func_name=self.func_name) var = sympy.Symbol(diff_eq.var_name, real=True) try: s_df_part = tools.timeout(self.timeout)(self.solve)(diff_eq, var) except KeyboardInterrupt: raise errors.DiffEqError( f'{self.__class__} solve {self.f} failed, because ' f'symbolic differentiation of SymPy timeout due to {self.timeout} s limit. ' f'Instead, you can use {ExpEulerAuto} to make Exponential Euler ' f'integration due to due to it is capable of ' f'performing automatic differentiation.') # update expression update = var + s_df_part # The actual update step self.code_lines.append( f' {diff_eq.var_name}_new = {analysis_by_sympy.sympy2str(update)}' ) self.code_lines.append('') self.code_lines.append( f' return {", ".join([f"{v}_new" for v in self.variables])}') self.integral = utils.compile_code( code_scope={k: v for k, v in self.code_scope.items()}, code_lines=self.code_lines, show_code=self.show_code, func_name=self.func_name) if hasattr(self.f, '__self__'): host = self.f.__self__ self.integral = self.integral.__get__(host, host.__class__)