Skip to content

Commit

Permalink
Merge pull request #34 from brian-team/new_run_system
Browse files Browse the repository at this point in the history
New run system (changed namespace handling)
  • Loading branch information
thesamovar committed May 3, 2013
2 parents 0800ffd + 8bd6d70 commit 4c07014
Show file tree
Hide file tree
Showing 29 changed files with 1,279 additions and 668 deletions.
3 changes: 2 additions & 1 deletion brian2/codegen/functions/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
__all__ = ['Function', 'SimpleFunction', 'make_function']

class Function(object):
def __init__(self, pyfunc):
def __init__(self, pyfunc, sympy_func=None):
self.pyfunc = pyfunc
self.sympy_func = sympy_func

'''
User-defined function to work with code generation
Expand Down
82 changes: 76 additions & 6 deletions brian2/codegen/functions/numpyfunctions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import sympy
from sympy import Function as sympy_Function
from sympy.core import power as sympy_power
from sympy.core import mod as sympy_mod
import numpy as np
from numpy.random import randn

import brian2.units.unitsafefunctions as unitsafe

from .base import Function

__all__ = ['RandnFunction', 'FunctionWrapper']
__all__ = ['RandnFunction', 'FunctionWrapper', 'DEFAULT_FUNCTIONS']

class RandnFunction(Function):
'''
Expand Down Expand Up @@ -79,13 +86,15 @@ class FunctionWrapper(Function):
py_name : str, optional
The name of the python function, in case it is not unambiguously
defined by `pyfunc`. For example, the ``abs`` function in numpy is
actually named ``absolute`` but we want to use the name ``abs``.
actually named ``absolute`` but we want to use the name ``abs``.
cpp_name : str, optional
The name of the corresponding function in C++, in case it is different.
The name of the corresponding function in C++, in case it is different.
sympy_func : sympy function, optional
The corresponding sympy function, if it exists.
'''
# TODO: How to make this easily extendable for other languages?
def __init__(self, pyfunc, py_name=None, cpp_name=None):
Function.__init__(self, pyfunc)
def __init__(self, pyfunc, py_name=None, cpp_name=None, sympy_func=None):
Function.__init__(self, pyfunc, sympy_func)
if py_name is None:
py_name = pyfunc.__name__
self.py_name = py_name
Expand All @@ -98,4 +107,65 @@ def code_cpp(self, language, var):
hashdefine_code = '#define {python_name} {cpp_name}'.format(python_name=self.py_name,
cpp_name=self.cpp_name)
return {'support_code': '',
'hashdefine_code': hashdefine_code}
'hashdefine_code': hashdefine_code}

def _get_default_functions():

# sympy does not have a log10 function, so let's define one
class log10(sympy_Function):
nargs = 1

@classmethod
def eval(cls, args):
return sympy.functions.elementary.exponential.log(args, 10)

functions = {
# numpy functions that have the same name in numpy and math.h
'cos': FunctionWrapper(unitsafe.cos,
sympy_func=sympy.functions.elementary.trigonometric.cos),
'sin': FunctionWrapper(unitsafe.sin,
sympy_func=sympy.functions.elementary.trigonometric.sin),
'tan': FunctionWrapper(unitsafe.tan,
sympy_func=sympy.functions.elementary.trigonometric.tan),
'cosh': FunctionWrapper(unitsafe.cosh,
sympy_func=sympy.functions.elementary.hyperbolic.cosh),
'sinh': FunctionWrapper(unitsafe.sinh,
sympy_func=sympy.functions.elementary.hyperbolic.sinh),
'tanh': FunctionWrapper(unitsafe.tanh,
sympy_func=sympy.functions.elementary.hyperbolic.tanh),
'exp': FunctionWrapper(unitsafe.exp,
sympy_func=sympy.functions.elementary.exponential.exp),
'log': FunctionWrapper(unitsafe.log,
sympy_func=sympy.functions.elementary.exponential.log),
'log10': FunctionWrapper(unitsafe.log10,
sympy_func=log10),
'sqrt': FunctionWrapper(np.sqrt,
sympy_func=sympy.functions.elementary.miscellaneous.sqrt),
'ceil': FunctionWrapper(np.ceil,
sympy_func=sympy.functions.elementary.integers.ceiling),
'floor': FunctionWrapper(np.floor,
sympy_func=sympy.functions.elementary.integers.floor),
# numpy functions that have a different name in numpy and math.h
'arccos': FunctionWrapper(unitsafe.arccos,
cpp_name='acos',
sympy_func=sympy.functions.elementary.trigonometric.acos),
'arcsin': FunctionWrapper(unitsafe.arcsin,
cpp_name='asin',
sympy_func=sympy.functions.elementary.trigonometric.asin),
'arctan': FunctionWrapper(unitsafe.arctan,
cpp_name='atan',
sympy_func=sympy.functions.elementary.trigonometric.atan),
'power': FunctionWrapper(np.power,
cpp_name='pow',
sympy_func=sympy_power.Pow),
'abs': FunctionWrapper(np.abs, py_name='abs',
cpp_name='fabs',
sympy_func=sympy.functions.elementary.complexes.Abs),
'mod': FunctionWrapper(np.mod, py_name='mod',
cpp_name='fmod',
sympy_func=sympy_mod.Mod)
}

return functions

DEFAULT_FUNCTIONS = _get_default_functions()
2 changes: 1 addition & 1 deletion brian2/codegen/languages/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def __call__(self, **kwds):
for name, spec in self.specifiers.iteritems():
if isinstance(spec, Value):
value = spec.get_value()
self.namespace.update({name: value})
self.namespace[name] = value
# if it is a type that has a length, add a variable called
# '_num'+name with its length
try:
Expand Down
12 changes: 11 additions & 1 deletion brian2/codegen/languages/cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import numpy

from brian2.utils.stringtools import deindent
from brian2.utils.parsing import parse_to_sympy
from brian2.codegen.parsing import parse_to_sympy
from brian2.codegen.functions.base import Function
from brian2.utils.logger import get_logger

Expand Down Expand Up @@ -179,6 +179,16 @@ def translate_statement_sequence(self, statements, specifiers, namespace, indice
speccode = spec.code(self, var)
support_code += '\n' + deindent(speccode['support_code'])
hash_defines += deindent(speccode['hashdefine_code'])
# add the Python function with a leading underscore, if it
# exists. This allows the function to make use of the Python
# function via weave if necessary (e.g. in the case of randn)
if not spec.pyfunc is None:
pyfunc_name = '_' + var
if pyfunc_name in namespace:
logger.warn(('Namespace already contains function %s, '
'not replacing it') % pyfunc_name)
else:
namespace[pyfunc_name] = spec.pyfunc

# delete the user-defined functions from the namespace
for func in user_functions:
Expand Down
2 changes: 1 addition & 1 deletion brian2/codegen/languages/python_numexpr.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from brian2.utils.parsing import parse_to_sympy
from brian2.codegen.parsing import parse_to_sympy

try:
import numexpr
Expand Down
83 changes: 80 additions & 3 deletions brian2/utils/parsing.py → brian2/codegen/parsing.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
'''
Utility functions for parsing expressions to sympy.
Utility functions for parsing expressions and statements.
'''
import re
from StringIO import StringIO

import sympy
from sympy.parsing.sympy_tokenize import (generate_tokens, untokenize, NUMBER,
NAME, OP)
from .functions.numpyfunctions import DEFAULT_FUNCTIONS

SYMPY_DICT = {'I': sympy.I,
'Float': sympy.Float,
'Integer': sympy.Integer,
'Symbol': sympy.Symbol}

def parse_statement(code):
'''
Parses a single line of code into "var op expr".
Parameters
----------
code : str
A string containing a single statement of the form ``var op expr``.
Returns
-------
var, op, expr : str, str, str
The three parts of the statement.
Examples
--------
>>> parse_statement('v = -65*mV')
('v', '=', '-65*mV')
>>> parse_statement('v += dt*(-v/tau)')
('v', '+=', 'dt*(-v/tau)')
'''
m = re.search(r'(\+|\-|\*|/|//|%|\*\*|>>|<<|&|\^|\|)?=', code)
if not m:
raise ValueError("Could not extract statement from: " + code)
start, end = m.start(), m.end()
op = code[start:end].strip()
var = code[:start].strip()
expr = code[end:].strip()
# var should be a single word
if len(re.findall(r'^[A-Za-z_][A-Za-z0-9_]*$', var))!=1:
raise ValueError("LHS in statement must be single variable name, line: " + code)

return var, op, expr

def parse_to_sympy(expr, local_dict=None):
'''
Parses a string into a sympy expression. The reason for not using `sympify`
Expand Down Expand Up @@ -47,7 +84,10 @@ def parse_to_sympy(expr, local_dict=None):
https://github.com/sympy/sympy/blob/master/LICENSE
'''
if local_dict is None:
local_dict = {}
# use the standard functions
local_dict = dict((name, f.sympy_func) for
name, f in DEFAULT_FUNCTIONS.iteritems()
if f.sympy_func is not None)

tokens = generate_tokens(StringIO(expr).readline)

Expand Down Expand Up @@ -80,9 +120,14 @@ def parse_to_sympy(expr, local_dict=None):
continue

result.extend([
# TODO: We always assume that variables are real, right?
(NAME, 'Symbol'),
(OP, '('),
(NAME, repr(str(name))),
(OP, ','),
(NAME, 'real'),
(OP, '='),
(NAME, 'True'),
(OP, ')'),
])
elif toknum == OP:
Expand All @@ -103,4 +148,36 @@ def parse_to_sympy(expr, local_dict=None):
raise ValueError('Expression "%s" could not be parsed: %s' %
(expr, str(ex)))

return s_expr
return s_expr


def sympy_to_str(sympy_expr):
'''
Converts a sympy expression into a string. This could be as easy as
``str(sympy_exp)`` but it is possible that the sympy expression contains
functions like ``Abs`` (for example, if an expression such as
``sqrt(x**2)`` appeared somewhere). We do want to re-translate ``Abs`` into
``abs`` in this case.
Parameters
----------
sympy_expr : sympy.core.expr.Expr
The expression that should be converted to a string.
Returns
str_expr : str
A string representing the sympy expression.
'''

# replace the standard functions by our names if necessary
replacements = dict((f.sympy_func, sympy.Function(name)) for
name, f in DEFAULT_FUNCTIONS.iteritems()
if f.sympy_func is not None and isinstance(f.sympy_func,
sympy.FunctionClass)
and str(f.sympy_func) != name)

sympy_expr = sympy_expr.subs(replacements)

return str(sympy_expr)


13 changes: 2 additions & 11 deletions brian2/codegen/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
* The dtype to use for newly created variables
* The language to translate to
'''
import re
from numpy import float64

from brian2.core.specifiers import Value, ArrayVariable, Subexpression, Index
from brian2.utils.stringtools import (deindent, strip_empty_lines, indent,
get_identifiers)

from .statements import Statement
from .parsing import parse_statement

__all__ = ['translate', 'make_statements']

Expand Down Expand Up @@ -99,16 +99,7 @@ def make_statements(code, specifiers, dtype):
if hasattr(spec, 'get_value'))
for line in lines:
# parse statement into "var op expr"
m = re.search(r'(\+|\-|\*|/|//|%|\*\*|>>|<<|&|\^|\|)?=', line.code)
if not m:
raise ValueError("Could not extract statement from: "+line.code)
start, end = m.start(), m.end()
op = line.code[start:end].strip()
var = line.code[:start].strip()
expr = line.code[end:].strip()
# var should be a single word
if len(re.findall(r'^[A-Za-z_][A-Za-z0-9_]*$', var))!=1:
raise ValueError("LHS in statement must be single variable name, line: "+line.code)
var, op, expr = parse_statement(line.code)
if op=='=' and var not in defined:
op = ':='
defined.add(var)
Expand Down
31 changes: 24 additions & 7 deletions brian2/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,35 @@ def __init__(self, when=None, name=None):
#: Whether or not `MagicNetwork` is invalidated when a new `BrianObject` of this type is created or removed
invalidates_magic_network = True

def prepare(self):
def pre_run(self, namespace):
'''
Optional method to prepare data for the first time.
Optional method to prepare the object before a run. Receives the
`namespace` in which the object should be run (either the locals/globals
or an explicitly defined namespace). This namespace has to be passed
as an ``additional_namespace`` argument to calls of
`CompoundNamespace.resolve` or `CompoundNamespace.resolve_all`.
Called by `Network.prepare`. Note that this method will not be
called until just before the Network is about to be run, but may be
called more than once even if the object has already been prepared, so
the class should keep track of whether it has already been prepared or
not.
Called by `Network.pre_run` before the main simulation loop is started.
Objects such as `NeuronGroup` will generate internal objects such as
state updaters in this method, taking into account changes in the
namespace or in constant parameter values.
Parameters
----------
namespace : (str, dict-like)
A (name, namespace) tuple with a description and the namespace in
which the `BrianObject` should be executed.
'''
pass

def post_run(self):
'''
Optional method to do work after a run is finished.
Called by `Network.post_run` after the main simulation loop terminated.
'''
pass

def update(self):
'''
Every `BrianObject` should define an ``update()`` method which is called every time step.
Expand Down

0 comments on commit 4c07014

Please sign in to comment.