def test_is_boolean_expression(): # dummy "Variable" class Var = namedtuple("Var", ['is_boolean']) # dummy function object class Func(object): def __init__(self, returns_bool=False): self._returns_bool = returns_bool # variables / functions a = Constant('a', value=True) b = Constant('b', value=False) c = Constant('c', value=5) f = Func(returns_bool=True) g = Func(returns_bool=False) s1 = Var(is_boolean=True) s2 = Var(is_boolean=False) variables = {'a': a, 'b': b, 'c': c, 'f': f, 'g': g, 's1': s1, 's2': s2} EVF = [ (True, 'a or b'), (False, 'c'), (False, 's2'), (False, 'g(s1)'), (True, 's2 > c'), (True, 'c > 5'), (True, 'True'), (True, 'a<b'), (True, 'not (a>=b)'), (False, 'a+b'), (True, 'f(c)'), (False, 'g(c)'), ( True, 'f(c) or a<b and s1', ), ] for expect, expr in EVF: ret_val = is_boolean_expression(expr, variables) if expect != ret_val: raise AssertionError( ('is_boolean_expression(%r) returned %s, ' 'but was supposed to return %s') % (expr, ret_val, expect)) with pytest.raises(SyntaxError): is_boolean_expression('a<b and c', variables) with pytest.raises(SyntaxError): is_boolean_expression('a or foo', variables) with pytest.raises(SyntaxError): is_boolean_expression( 'ot a', # typo variables) with pytest.raises(SyntaxError): is_boolean_expression('g(c) and f(a)', variables)
def test_warning_internal_variables(): group1 = SimpleGroup(namespace=None, variables={'N': Constant('N', 5)}) group2 = SimpleGroup(namespace=None, variables={'N': Constant('N', 7)}) with catch_logs() as l: group1.resolve_all(['N'], run_namespace={'N': 5}) # should not raise a warning assert len(l) == 0, 'got warnings: %s' % str(l) with catch_logs() as l: group2.resolve_all(['N'], run_namespace={'N': 5}) # should raise a warning assert len(l) == 1, 'got warnings: %s' % str(l) assert l[0][1].endswith('.resolution_conflict')
def test_apply_loop_invariant_optimisation(): variables = {'v': Variable('v', scalar=False), 'w': Variable('w', scalar=False), 'dt': Constant('dt', dimensions=second.dim, value=0.1*ms), 'tau': Constant('tau', dimensions=second.dim, value=10*ms), 'exp': DEFAULT_FUNCTIONS['exp']} statements = [Statement('v', '=', 'dt*w*exp(-dt/tau)/tau + v*exp(-dt/tau)', '', np.float32), Statement('w', '=', 'w*exp(-dt/tau)', '', np.float32)] scalar, vector = optimise_statements([], statements, variables) # The optimisation should pull out at least exp(-dt / tau) assert len(scalar) >= 1 assert np.issubdtype(scalar[0].dtype, np.floating) assert scalar[0].var == '_lio_1' assert len(vector) == 2 assert all('_lio_' in stmt.expr for stmt in vector)
def test_apply_loop_invariant_optimisation_constant_evaluation(): variables = { 'v1': Variable('v1', scalar=False), 'v2': Variable('v2', scalar=False), 'i1': Variable('i1', scalar=False, dtype=int), 'N': Constant('N', 10), 's1': Variable('s1', scalar=True, dtype=float), 's2': Variable('s2', scalar=True, dtype=float), 'exp': DEFAULT_FUNCTIONS['exp'] } statements = [ Statement('v1', '=', 'v1 * (1 + 2 + 3)', '', np.float), Statement('v1', '=', 'exp(N)*v1', '', np.float), Statement('v1', '=', 'exp(0)*v1', '', np.float), ] scalar, vector = optimise_statements([], statements, variables) # exp(N) should be pulled out of the vector statements, the rest should be # evaluated in place assert len(scalar) == 1 assert scalar[0].expr == 'exp(N)' assert len(vector) == 3 expr = vector[0].expr.replace(' ', '') assert expr == '_lio_1*v1' or 'v1*_lio_1' expr = vector[1].expr.replace(' ', '') assert expr == '6.0*v1' or 'v1*6.0' assert vector[2].expr == 'v1'
def test_apply_loop_invariant_optimisation_no_optimisation(): variables = { 'v1': Variable('v1', scalar=False), 'v2': Variable('v2', scalar=False), 'N': Constant('N', 10), 's1': Variable('s1', scalar=True, dtype=float), 's2': Variable('s2', scalar=True, dtype=float), 'rand': DEFAULT_FUNCTIONS['rand'] } statements = [ # This hould not be simplified to 0! Statement('v1', '=', 'rand() - rand()', '', np.float), Statement('v1', '=', '3*rand() - 3*rand()', '', np.float), Statement('v1', '=', '3*rand() - ((1+2)*rand())', '', np.float), # This should not pull out rand()*N Statement('v1', '=', 's1*rand()*N', '', np.float), Statement('v1', '=', 's2*rand()*N', '', np.float), # This is not important mathematically, but it would change the numbers # that are generated Statement('v1', '=', '0*rand()*N', '', np.float), Statement('v1', '=', '0/rand()*N', '', np.float) ] scalar, vector = optimise_statements([], statements, variables) for vs in vector[:3]: assert vs.expr.count( 'rand()' ) == 2, 'Expression should still contain two rand() calls, but got ' + str( vs) for vs in vector[3:]: assert vs.expr.count( 'rand()' ) == 1, 'Expression should still contain a rand() call, but got ' + str( vs)
def test_apply_loop_invariant_optimisation_integer(): variables = { 'v': Variable('v', scalar=False), 'N': Constant('N', 10), 'b': Variable('b', scalar=True, dtype=int), 'c': Variable('c', scalar=True, dtype=int), 'd': Variable('d', scalar=True, dtype=int), 'y': Variable('y', scalar=True, dtype=float), 'z': Variable('z', scalar=True, dtype=float), 'w': Variable('w', scalar=True, dtype=float), } statements = [ Statement('v', '=', 'v % (2*3*N)', '', np.float32), # integer version doesn't get rewritten but float version does Statement('a', ':=', 'b//(c//d)', '', int), Statement('x', ':=', 'y/(z/w)', '', float), ] scalar, vector = optimise_statements([], statements, variables) assert len(scalar) == 3 assert np.issubdtype(scalar[0].dtype, np.signedinteger) assert scalar[0].var == '_lio_1' expr = scalar[0].expr.replace(' ', '') assert expr == '6*N' or expr == 'N*6' assert np.issubdtype(scalar[1].dtype, np.signedinteger) assert scalar[1].var == '_lio_2' expr = scalar[1].expr.replace(' ', '') assert expr == 'b//(c//d)' assert np.issubdtype(scalar[2].dtype, np.floating) assert scalar[2].var == '_lio_3' expr = scalar[2].expr.replace(' ', '') assert expr == '(y*w)/z' or expr == '(w*y)/z'
def test_apply_loop_invariant_optimisation_boolean(): variables = {'v1': Variable('v1', scalar=False), 'v2': Variable('v2', scalar=False), 'N': Constant('N', 10), 'b': Variable('b', scalar=True, dtype=bool), 'c': Variable('c', scalar=True, dtype=bool), 'int': DEFAULT_FUNCTIONS['int'], 'foo': Function(lambda x: None, arg_units=[Unit(1)], return_unit=Unit(1), arg_types=['boolean'], return_type='float', stateless=False) } # The calls for "foo" cannot be pulled out, since foo is marked as stateful statements = [Statement('v1', '=', '1.0*int(b and c)', '', np.float32), Statement('v1', '=', '1.0*foo(b and c)', '', np.float32), Statement('v2', '=', 'int(not b and True)', '', np.float32), Statement('v2', '=', 'foo(not b and True)', '', np.float32) ] scalar, vector = optimise_statements([], statements, variables) assert len(scalar) == 4 assert scalar[0].expr == '1.0 * int(b and c)' assert scalar[1].expr == 'b and c' assert scalar[2].expr == 'int((not b) and True)' assert scalar[3].expr == '(not b) and True' assert len(vector) == 4 assert vector[0].expr == '_lio_1' assert vector[1].expr == 'foo(_lio_2)' assert vector[2].expr == '_lio_3' assert vector[3].expr == 'foo(_lio_4)'
def test_value_from_expression(): # This function is used to get the value of an exponent, necessary for unit checking constants = {'c': 3} # dummy class class C(object): pass variables = {'s_constant_scalar': C(), 's_non_constant': C(), 's_non_scalar': C()} variables['s_constant_scalar'].scalar = True variables['s_constant_scalar'].constant = True variables['s_constant_scalar'].get_value = lambda: 2.0 variables['s_non_scalar'].constant = True variables['s_non_constant'].scalar = True variables['c'] = Constant('c', value=3) expressions = ['1', '-0.5', 'c', '2**c', '(c + 3) * 5', 'c + s_constant_scalar', 'True', 'False'] for expr in expressions: eval_expr = expr.replace('s_constant_scalar', 's_constant_scalar.get_value()') assert float(eval(eval_expr, variables, constants)) == _get_value_from_expression(expr, variables) wrong_expressions = ['s_non_constant', 's_non_scalar', 'c or True'] for expr in wrong_expressions: with pytest.raises(SyntaxError): _get_value_from_expression(expr, variables)
def test_apply_loop_invariant_optimisation_integer(): variables = { 'v': Variable('v', Unit(1), scalar=False), 'N': Constant('N', Unit(1), 10) } statements = [Statement('v', '=', 'v % (2*3*N)', '', np.float32)] scalar, vector = apply_loop_invariant_optimisations( statements, variables, np.float64) # The optimisation should not pull out 2*N assert len(scalar) == 0
def test_apply_loop_invariant_optimisation(): variables = { 'v': Variable('v', Unit(1), scalar=False), 'w': Variable('w', Unit(1), scalar=False), 'dt': Constant('dt', second, 0.1 * ms), 'tau': Constant('tau', second, 10 * ms), 'exp': DEFAULT_FUNCTIONS['exp'] } statements = [ Statement('v', '=', 'dt*w*exp(-dt/tau)/tau + v*exp(-dt/tau)', '', np.float32), Statement('w', '=', 'w*exp(-dt/tau)', '', np.float32) ] scalar, vector = apply_loop_invariant_optimisations( statements, variables, np.float64) # The optimisation should pull out exp(-dt / tau) assert len(scalar) == 1 assert scalar[0].dtype == np.float64 # We asked for this dtype above assert scalar[0].var == '_lio_const_1' assert len(vector) == 2 assert all('_lio_const_1' in stmt.expr for stmt in vector)
def test_apply_loop_invariant_optimisation_simplification(): variables = { 'v1': Variable('v1', scalar=False), 'v2': Variable('v2', scalar=False), 'i1': Variable('i1', scalar=False, dtype=int), 'N': Constant('N', 10) } statements = [ # Should be simplified to 0.0 Statement('v1', '=', 'v1 - v1', '', np.float), Statement('v1', '=', 'N*v1 - N*v1', '', np.float), Statement('v1', '=', 'v1*N * 0', '', np.float), Statement('v1', '=', 'v1 * 0', '', np.float), Statement('v1', '=', 'v1 * 0.0', '', np.float), Statement('v1', '=', '0.0 / (v1*N)', '', np.float), # Should be simplified to 0 Statement('i1', '=', 'i1*N * 0', '', np.int), Statement('i1', '=', '0 * i1', '', np.int), Statement('i1', '=', '0 * i1*N', '', np.int), Statement('i1', '=', 'i1 * 0', '', np.int), # Should be simplified to v1*N Statement('v2', '=', '0 + v1*N', '', np.float), Statement('v2', '=', 'v1*N + 0.0', '', np.float), Statement('v2', '=', 'v1*N - 0', '', np.float), Statement('v2', '=', 'v1*N - 0.0', '', np.float), Statement('v2', '=', '1 * v1*N', '', np.float), Statement('v2', '=', '1.0 * v1*N', '', np.float), Statement('v2', '=', 'v1*N / 1.0', '', np.float), Statement('v2', '=', 'v1*N / 1', '', np.float), # Should be simplified to i1 Statement('i1', '=', 'i1*1', '', int), Statement('i1', '=', 'i1//1', '', int), Statement('i1', '=', 'i1+0', '', int), Statement('i1', '=', '0+i1', '', int), Statement('i1', '=', 'i1-0', '', int), # Should *not* be simplified (because it would change the type, # important for integer division, for example) Statement('v1', '=', 'i1*1.0', '', float), Statement('v1', '=', '1.0*i1', '', float), Statement('v1', '=', 'i1/1.0', '', float), Statement('v1', '=', 'i1/1', '', float), Statement('v1', '=', 'i1+0.0', '', float), Statement('v1', '=', '0.0+i1', '', float), Statement('v1', '=', 'i1-0.0', '', float), ## Should *not* be simplified, flooring division by 1 changes the value Statement('v1', '=', 'v2//1.0', '', float), Statement('i1', '=', 'i1//1.0', '', float) # changes type ] scalar, vector = optimise_statements([], statements, variables) assert len(scalar) == 0 for s in vector[:6]: assert s.expr == '0.0' for s in vector[6:10]: assert s.expr == '0', s.expr # integer for s in vector[10:18]: expr = s.expr.replace(' ', '') assert expr == 'v1*N' or expr == 'N*v1' for s in vector[18:23]: expr = s.expr.replace(' ', '') assert expr == 'i1' for s in vector[23:27]: expr = s.expr.replace(' ', '') assert expr == '1.0*i1' or expr == 'i1*1.0' or expr == 'i1/1.0' for s in vector[27:30]: expr = s.expr.replace(' ', '') assert expr == '0.0+i1' or expr == 'i1+0.0' for s in vector[30:31]: expr = s.expr.replace(' ', '') assert expr == 'v2//1.0' or expr == 'v2//1' for s in vector[31:]: expr = s.expr.replace(' ', '') assert expr == 'i1//1.0'
def _resolve_external(self, identifier, run_namespace, user_identifier=True, internal_variable=None): ''' Resolve an external identifier in the context of a `Group`. If the `Group` declares an explicit namespace, this namespace is used in addition to the standard namespace for units and functions. Additionally, the namespace in the `run_namespace` argument (i.e. the namespace provided to `Network.run`) is used. Parameters ---------- identifier : str The name to resolve. group : `Group` The group that potentially defines an explicit namespace for looking up external names. run_namespace : dict A namespace (mapping from strings to objects), as provided as an argument to the `Network.run` function or returned by `get_local_namespace`. user_identifier : bool, optional Whether this is an identifier that was used by the user (and not something automatically generated that the user might not even know about). Will be used to determine whether to display a warning in the case of namespace clashes. Defaults to ``True``. internal_variable : `Variable`, optional The internal variable object that corresponds to this name (if any). This is used to give warnings if it also corresponds to a variable from an external namespace. ''' # We save tuples of (namespace description, referred object) to # give meaningful warnings in case of duplicate definitions matches = [] namespaces = OrderedDict() # Default namespaces (units and functions) namespaces['constants'] = DEFAULT_CONSTANTS namespaces['units'] = DEFAULT_UNITS namespaces['functions'] = DEFAULT_FUNCTIONS if getattr(self, 'namespace', None) is not None: namespaces['group-specific'] = self.namespace # explicit or implicit run namespace namespaces['run'] = run_namespace for description, namespace in namespaces.iteritems(): if identifier in namespace: match = namespace[identifier] if ((isinstance(match, (numbers.Number, np.ndarray, np.number, Function, Variable))) or (inspect.isfunction(match) and hasattr(match, '_arg_units') and hasattr(match, '_return_unit'))): matches.append((description, match)) if len(matches) == 0: # No match at all if internal_variable is not None: return None else: raise KeyError(('The identifier "%s" could not be resolved.') % (identifier)) elif len(matches) > 1: # Possibly, all matches refer to the same object first_obj = matches[0][1] found_mismatch = False for m in matches: if _same_value(m[1], first_obj): continue if _same_function(m[1], first_obj): continue try: proxy = weakref.proxy(first_obj) if m[1] is proxy: continue except TypeError: pass # Found a mismatch found_mismatch = True break if found_mismatch and user_identifier and internal_variable is None: _conflict_warning( ('The name "%s" refers to different objects ' 'in different namespaces used for resolving ' 'names in the context of group "%s". ' 'Will use the object from the %s namespace ' 'with the value %s,') % (identifier, getattr(self, 'name', '<unknown>'), matches[0][0], _display_value(first_obj)), matches[1:]) if internal_variable is not None and user_identifier: # Filter out matches that are identical (a typical case being an # externally defined "N" with the the number of neurons and a later # use of "N" in an expression (which refers to the internal variable # storing the number of neurons in the group) if isinstance(internal_variable, Constant): filtered_matches = [] for match in matches: if not _same_value(match[1], internal_variable): filtered_matches.append(match) else: filtered_matches = matches if len(filtered_matches) == 0: pass # Nothing to warn about else: warning_message = ('"{name}" is an internal variable of group ' '"{group}", but also exists in the ') if len(matches) == 1: warning_message += ('{namespace} namespace with the value ' '{value}. ').format( namespace=filtered_matches[0][0], value=_display_value( filtered_matches[0][1])) else: warning_message += ('following namespaces: ' '{namespaces}. ').format( namespaces=' ,'.join( match[0] for match in filtered_matches)) warning_message += 'The internal variable will be used.' logger.warn(warning_message.format(name=identifier, group=self.name), 'Group.resolve.resolution_conflict', once=True) if internal_variable is not None: return None # We were only interested in the warnings above # use the first match (according to resolution order) resolved = matches[0][1] # Replace pure Python functions by a Functions object if callable(resolved) and not isinstance(resolved, Function): resolved = Function(resolved, stateless=False) if not isinstance(resolved, (Function, Variable)): # Wrap the value in a Constant object unit = get_unit(resolved) value = np.asarray(resolved) if value.shape != (): raise KeyError('Variable %s was found in the namespace, but is' ' not a scalar value' % identifier) resolved = Constant(identifier, unit=unit, value=value) return resolved
def _resolve_external(self, identifier, run_namespace=None, level=0, do_warn=True): ''' Resolve an external identifier in the context of a `Group`. If the `Group` declares an explicit namespace, this namespace is used in addition to the standard namespace for units and functions. Additionally, the namespace in the `run_namespace` argument (i.e. the namespace provided to `Network.run`) or, if this argument is unspecified, the implicit namespace of surrounding variables in the stack frame where the original call was made is used (to determine this stack frame, the `level` argument has to be set correctly). Parameters ---------- identifier : str The name to resolve. group : `Group` The group that potentially defines an explicit namespace for looking up external names. run_namespace : dict, optional A namespace (mapping from strings to objects), as provided as an argument to the `Network.run` function. level : int, optional How far to go up in the stack to find the calling frame. do_warn : int, optional Whether to display a warning if an identifier resolves to different objects in different namespaces. Defaults to ``True``. ''' # We save tuples of (namespace description, referred object) to # give meaningful warnings in case of duplicate definitions matches = [] namespaces = OrderedDict() # Default namespaces (units and functions) namespaces['constants'] = DEFAULT_CONSTANTS namespaces['units'] = DEFAULT_UNITS namespaces['functions'] = DEFAULT_FUNCTIONS if getattr(self, 'namespace', None) is not None: namespaces['group-specific'] = self.namespace # explicit or implicit run namespace if run_namespace is not None: namespaces['run'] = run_namespace else: namespaces['implicit'] = get_local_namespace(level + 1) for description, namespace in namespaces.iteritems(): if identifier in namespace: matches.append((description, namespace[identifier])) if len(matches) == 0: # No match at all raise KeyError( ('The identifier "%s" could not be resolved.') % (identifier)) elif len(matches) > 1: # Possibly, all matches refer to the same object first_obj = matches[0][1] found_mismatch = False for m in matches: if _same_value(m[1], first_obj): continue if _same_function(m[1], first_obj): continue try: proxy = weakref.proxy(first_obj) if m[1] is proxy: continue except TypeError: pass # Found a mismatch found_mismatch = True break if found_mismatch and do_warn: _conflict_warning( ('The name "%s" refers to different objects ' 'in different namespaces used for resolving ' 'names in the context of group "%s". ' 'Will use the object from the %s namespace ' 'with the value %r') % (identifier, getattr( self, 'name', '<unknown>'), matches[0][0], first_obj), matches[1:]) # use the first match (according to resolution order) resolved = matches[0][1] # Replace pure Python functions by a Functions object if callable(resolved) and not isinstance(resolved, Function): resolved = Function(resolved) if not isinstance(resolved, (Function, Variable)): # Wrap the value in a Constant object unit = get_unit(resolved) value = np.asarray(resolved) if value.shape != (): raise KeyError('Variable %s was found in the namespace, but is' ' not a scalar value' % identifier) resolved = Constant(identifier, unit=unit, value=value) return resolved