def _latex(self, *args): equations = [] t = sympy.Symbol('t') for eq in self._equations.itervalues(): # do not use SingleEquations._latex here as we want nice alignment varname = sympy.Symbol(eq.varname) if eq.type == DIFFERENTIAL_EQUATION: lhs = r'\frac{\mathrm{d}' + sympy.latex( varname) + r'}{\mathrm{d}t}' else: # Normal equation or parameter lhs = varname if not eq.type == PARAMETER: rhs = str_to_sympy(eq.expr.code) if len(eq.flags): flag_str = ', flags: ' + ', '.join(eq.flags) else: flag_str = '' if eq.type == PARAMETER: eq_latex = r'%s &&& \text{(unit: $%s$%s)}' % ( sympy.latex(lhs), sympy.latex(get_unit(eq.dim)), flag_str) else: eq_latex = r'%s &= %s && \text{(unit of $%s$: $%s$%s)}' % ( sympy.latex(lhs), sympy.latex(rhs), sympy.latex(varname), sympy.latex(get_unit(eq.dim)), flag_str) equations.append(eq_latex) return r'\begin{align*}' + (r'\\' + '\n').join(equations) + r'\end{align*}'
def check_dimensions(expression, dimensions, variables): ''' Compares the physical dimensions of an expression to expected dimensions in a given namespace. Parameters ---------- expression : str The expression to evaluate. dimensions : `Dimension` The expected physical dimensions for the `expression`. variables : dict Dictionary of all variables (including external constants) used in the `expression`. Raises ------ KeyError In case on of the identifiers cannot be resolved. DimensionMismatchError If an unit mismatch occurs during the evaluation. ''' expr_dims = parse_expression_dimensions(expression, variables) err_msg = ('Expression {expr} does not have the ' 'expected unit {expected}').format(expr=expression.strip(), expected=repr(get_unit(dimensions))) fail_for_dimension_mismatch(expr_dims, dimensions, err_msg)
def _latex(self, *args): equations = [] t = sympy.Symbol('t') for eq in self._equations.itervalues(): # do not use SingleEquations._latex here as we want nice alignment varname = sympy.Symbol(eq.varname) if eq.type == DIFFERENTIAL_EQUATION: lhs = r'\frac{\mathrm{d}' + sympy.latex(varname) + r'}{\mathrm{d}t}' else: # Normal equation or parameter lhs = varname if not eq.type == PARAMETER: rhs = str_to_sympy(eq.expr.code) if len(eq.flags): flag_str = ', flags: ' + ', '.join(eq.flags) else: flag_str = '' if eq.type == PARAMETER: eq_latex = r'%s &&& \text{(unit: $%s$%s)}' % (sympy.latex(lhs), sympy.latex(get_unit(eq.dim)), flag_str) else: eq_latex = r'%s &= %s && \text{(unit of $%s$: $%s$%s)}' % (sympy.latex(lhs), sympy.latex(rhs), sympy.latex(varname), sympy.latex(get_unit(eq.dim)), flag_str) equations.append(eq_latex) return r'\begin{align*}' + (r'\\' + '\n').join(equations) + r'\end{align*}'
def test_get_unit(): ''' Test get_unit and get_unit_fast ''' values = [3 * mV, np.array([1, 2]) * mV, np.arange(12).reshape(4, 3) * mV] for value in values: assert get_unit(value) == volt assert_quantity(get_unit_fast(value), 1, volt)
def test_get_unit(): ''' Test get_unit and get_unit_fast ''' values = [3 * mV, np.array([1, 2]) * mV, np.arange(12).reshape(4, 3) * mV] for value in values: unit = get_unit(value) assert isinstance(unit, Unit) assert unit == volt assert_quantity(get_unit_fast(value), 1, volt) values = [3 * amp/metre**2, np.array([1, 2]) * amp/metre**2, np.arange(12).reshape(4, 3) * amp/metre**2] for value in values: unit = get_unit(value) assert isinstance(unit, Unit) assert unit == amp/metre**2 assert float(unit) == 1. assert_quantity(get_unit_fast(value), 1, amp/metre**2)
def test_get_unit(): ''' Test get_unit ''' values = [(volt.dim, volt), (mV.dim, volt), ((amp / metre**2).dim, amp / metre**2)] for dim, expected_unit in values: unit = get_unit(dim) assert isinstance(unit, Unit) assert unit == expected_unit assert float(unit) == 1.
def test_get_unit(): ''' Test get_unit ''' values = [(volt.dim, volt), (mV.dim, volt), ((amp/metre**2).dim, amp/metre**2)] for dim, expected_unit in values: unit = get_unit(dim) assert isinstance(unit, Unit) assert unit == expected_unit assert float(unit) == 1.
def __repr__(self): s = '<' + self.type + ' ' + self.varname if not self.expr is None: s += ': ' + self.expr.code s += ' (Unit: ' + str(get_unit(self.dim)) if len(self.flags): s += ', flags: ' + ', '.join(self.flags) s += ')>' return s
def __init__(self, target, target_var, N, rate, weight, when='synapses', order=0): if target_var not in target.variables: raise KeyError('%s is not a variable of %s' % (target_var, target.name)) if isinstance(weight, basestring): weight = '(%s)' % weight else: weight_unit = get_unit(weight) weight = repr(weight) target_unit = target.variables[target_var].unit # This will be checked automatically in the abstract code as well # but doing an explicit check here allows for a clearer error # message if not have_same_dimensions(weight_unit, target_unit): raise DimensionMismatchError( ('The provided weight does not ' 'have the same unit as the ' 'target variable "%s"') % target_var, weight_unit.dim, target_unit.dim) binomial_sampling = BinomialFunction(N, rate * target.clock.dt, name='poissoninput_binomial*') code = '{targetvar} += {binomial}()*{weight}'.format( targetvar=target_var, binomial=binomial_sampling.name, weight=weight) self._stored_dt = target.dt_[:] # make a copy # FIXME: we need an explicit reference here for on-the-fly subgroups # For example: PoissonInput(group[:N], ...) self._group = target CodeRunner.__init__(self, group=target, template='stateupdate', code=code, user_code='', when=when, order=order, name='poissoninput*', clock=target.clock) self.variables = Variables(self) self.variables._add_variable(binomial_sampling.name, binomial_sampling)
def _init_2d(self): dimensions = self.dim unit = get_unit(dimensions) values = self.values dt = self.dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(i=1, t=second, result=unit) def timed_array_func(t, i): # We round according to the current defaultclock.dt K = _find_K(float(defaultclock.dt), dt) epsilon = dt / K time_step = np.clip(np.int_(np.round(np.asarray(t/epsilon)) / K), 0, len(values)-1) return Quantity(values[time_step, i], dim=dimensions) Function.__init__(self, pyfunc=timed_array_func) # we use dynamic implementations because we want to do upsampling # in a way that avoids rounding problems with the group's dt def create_numpy_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) n_values = len(values) epsilon = dt / K def unitless_timed_array_func(t, i): timestep = np.clip(np.int_(np.round(t/epsilon) / K), 0, n_values-1) return values[timestep, i] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit return unitless_timed_array_func self.implementations.add_dynamic_implementation('numpy', create_numpy_implementation) values_flat = self.values.astype(np.double, order='C', copy=False).ravel() namespace = lambda owner: {'%s_values' % self.name: values_flat} for target, (_, func_2d) in TimedArray.implementations.items(): self.implementations.add_dynamic_implementation(target, func_2d(self.values, self.dt, self.name), namespace=namespace, name=self.name)
def __init__(self, values, dt, name=None): if name is None: name = '_timedarray*' Nameable.__init__(self, name) unit = get_unit(values) values = np.asarray(values) self.values = values dt = float(dt) self.dt = dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(t=second, result=unit) def timed_array_func(t): i = np.clip(np.int_(np.float_(t) / dt + 0.5), 0, len(values)-1) return values[i] * unit Function.__init__(self, pyfunc=timed_array_func) # Implementation for numpy, without units def unitless_timed_array_func(t): i = np.clip(np.int_(np.float_(t) / dt + 0.5), 0, len(values)-1) return values[i] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit # Implementation for C++ cpp_code = {'support_code': ''' inline double _timedarray_%NAME%(const double t, const double _dt, const int _num_values, const double* _values) { int i = (int)(t/_dt + 0.5); // rounds to nearest int for positive values if(i<0) i = 0; if(i>=_num_values) i = _num_values-1; return _values[i]; } '''.replace('%NAME%', self.name), 'hashdefine_code': ''' #define %NAME%(t) _timedarray_%NAME%(t, _%NAME%_dt, _%NAME%_num_values, _%NAME%_values) '''.replace('%NAME%', self.name)} namespace = {'_%s_dt' % self.name: self.dt, '_%s_num_values' % self.name: len(self.values), '_%s_values' % self.name: self.values} add_implementations(self, codes={'cpp': cpp_code, 'numpy': unitless_timed_array_func}, namespaces={'cpp': namespace}, names={'cpp': self.name})
def __str__(self): if self.type == DIFFERENTIAL_EQUATION: s = 'd' + self.varname + '/dt' else: s = self.varname if not self.expr is None: s += ' = ' + str(self.expr) s += ' : ' + str(get_unit(self.dim)) if len(self.flags): s += ' (' + ', '.join(self.flags) + ')' return s
def __init__(self, values, dt, name=None): if name is None: name = '_timedarray*' Nameable.__init__(self, name) unit = get_unit(values) self.unit = unit values = np.asarray(values, dtype=np.double) self.values = values dt = float(dt) self.dt = dt if values.ndim == 1: self._init_1d() elif values.ndim == 2: self._init_2d() else: raise NotImplementedError(('Only 1d and 2d arrays are supported ' 'for TimedArray'))
def __init__(self, target, target_var, N, rate, weight, when='synapses', order=0): if target_var not in target.variables: raise KeyError('%s is not a variable of %s' % (target_var, target.name)) if isinstance(weight, basestring): weight = '(%s)' % weight else: weight_unit = get_unit(weight) weight = repr(weight) target_unit = target.variables[target_var].unit # This will be checked automatically in the abstract code as well # but doing an explicit check here allows for a clearer error # message if not have_same_dimensions(weight_unit, target_unit): raise DimensionMismatchError(('The provided weight does not ' 'have the same unit as the ' 'target variable "%s"') % target_var, weight_unit.dim, target_unit.dim) binomial_sampling = BinomialFunction(N, rate*target.clock.dt, name='poissoninput_binomial*') code = '{targetvar} += {binomial}()*{weight}'.format(targetvar=target_var, binomial=binomial_sampling.name, weight=weight) self._stored_dt = target.dt_[:] # make a copy # FIXME: we need an explicit reference here for on-the-fly subgroups # For example: PoissonInput(group[:N], ...) self._group = target CodeRunner.__init__(self, group=target, template='stateupdate', code=code, user_code='', when=when, order=order, name='poissoninput*', clock=target.clock ) self.variables = Variables(self) self.variables._add_variable(binomial_sampling.name, binomial_sampling)
def _repr_pretty_(self, p, cycle): ''' Pretty printing for ipython. ''' if cycle: # should never happen raise AssertionError('Cyclical call of SingleEquation._repr_pretty') if self.type == DIFFERENTIAL_EQUATION: p.text('d' + self.varname + '/dt') else: p.text(self.varname) if not self.expr is None: p.text(' = ') p.pretty(self.expr) p.text(' : ') p.pretty(get_unit(self.dim)) if len(self.flags): p.text(' (' + ', '.join(self.flags) + ')')
def dimensions_and_type_from_string(unit_string): ''' Returns the physical dimensions that results from evaluating a string like "siemens / metre ** 2", allowing for the special string "1" to signify dimensionless units, the string "boolean" for a boolean and "integer" for an integer variable. Parameters ---------- unit_string : str The string that should evaluate to a unit Returns ------- d, type : (`Dimension`, {FLOAT, INTEGER or BOOL}) The resulting physical dimensions and the type of the variable. Raises ------ ValueError If the string cannot be evaluated to a unit. ''' # Lazy import to avoid circular dependency from brian2.core.namespace import DEFAULT_UNITS global _base_units_with_alternatives global _base_units if _base_units_with_alternatives is None: base_units_for_dims = {} for unit_name, unit in reversed(DEFAULT_UNITS.items()): if float(unit) == 1.0 and repr(unit)[-1] not in ['2', '3']: if unit.dim in base_units_for_dims: if unit_name not in base_units_for_dims[unit.dim]: base_units_for_dims[unit.dim].append(unit_name) else: base_units_for_dims[unit.dim] = [repr(unit)] if unit_name != repr(unit): base_units_for_dims[unit.dim].append(unit_name) alternatives = sorted([tuple(values) for values in base_units_for_dims.itervalues()]) _base_units = dict([(v, DEFAULT_UNITS[v]) for values in alternatives for v in values]) # Create a string that lists all allowed base units alternative_strings = [] for units in alternatives: string = units[0] if len(units) > 1: string += ' ({other_units})'.format(other_units=', '.join(units[1:])) alternative_strings.append(string) _base_units_with_alternatives = ', '.join(alternative_strings) unit_string = unit_string.strip() # Special case: dimensionless unit if unit_string == '1': return DIMENSIONLESS, FLOAT # Another special case: boolean variable if unit_string == 'boolean': return DIMENSIONLESS, BOOLEAN if unit_string == 'bool': raise TypeError("Use 'boolean' not 'bool' as the unit for a boolean " "variable.") # Yet another special case: integer variable if unit_string == 'integer': return DIMENSIONLESS, INTEGER # Check first whether the expression only refers to base units identifiers = get_identifiers(unit_string) for identifier in identifiers: if identifier not in _base_units: if identifier in DEFAULT_UNITS: # A known unit, but not a base unit base_unit = get_unit(DEFAULT_UNITS[identifier].dim) if not repr(base_unit) in _base_units: # Make sure that we don't suggest a unit that is not allowed # (should not happen, normally) base_unit = Unit(1, dim=base_unit.dim) raise ValueError(('Unit specification refers to ' '"{identifier}", but this is not a base ' 'unit. Use "{base_unit}" ' 'instead.').format(identifier=identifier, base_unit=repr(base_unit))) else: # Not a known unit raise ValueError(('Unit specification refers to ' '"{identifier}", but this is not a base ' 'unit. The following base units are ' 'allowed: ' '{allowed_units}.').format(identifier=identifier, allowed_units=_base_units_with_alternatives)) try: evaluated_unit = eval(unit_string, _base_units) except Exception as ex: raise ValueError(('Could not interpret "%s" as a unit specification: ' '%s') % (unit_string, ex)) # Check whether the result is a unit if not isinstance(evaluated_unit, Unit): if isinstance(evaluated_unit, Quantity): raise ValueError(('"%s" does not evaluate to a unit but to a ' 'quantity -- make sure to only use units, e.g. ' '"siemens/metre**2" and not "1 * siemens/metre**2"') % unit_string) else: raise ValueError(('"%s" does not evaluate to a unit, the result ' 'has type %s instead.' % (unit_string, type(evaluated_unit)))) # No error has been raised, all good return evaluated_unit.dim, FLOAT
def _init_2d(self): dimensions = self.dim unit = get_unit(dimensions) values = self.values dt = self.dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(i=1, t=second, result=unit) def timed_array_func(t, i): # We round according to the current defaultclock.dt K = _find_K(float(defaultclock.dt), dt) epsilon = dt / K time_step = np.clip(np.int_(np.round(np.asarray(t/epsilon)) / K), 0, len(values)-1) return Quantity(values[time_step, i], dim=dimensions) Function.__init__(self, pyfunc=timed_array_func) # we use dynamic implementations because we want to do upsampling # in a way that avoids rounding problems with the group's dt def create_numpy_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) n_values = len(values) epsilon = dt / K def unitless_timed_array_func(t, i): timestep = np.clip(np.int_(np.round(t/epsilon) / K), 0, n_values-1) return values[timestep, i] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit return unitless_timed_array_func self.implementations.add_dynamic_implementation('numpy', create_numpy_implementation) def create_cpp_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) support_code = ''' static inline double %NAME%(const double t, const int i) { const double epsilon = %DT% / %K%; if (i < 0 || i >= %COLS%) return NAN; int timestep = (int)((t/epsilon + 0.5)/%K%); if(timestep < 0) timestep = 0; else if(timestep >= %ROWS%) timestep = %ROWS%-1; return _namespace%NAME%_values[timestep*%COLS% + i]; } ''' support_code = replace(support_code, {'%NAME%': self.name, '%DT%': '%.18f' % dt, '%K%': str(K), '%COLS%': str(self.values.shape[1]), '%ROWS%': str(self.values.shape[0])}) cpp_code = {'support_code': support_code} return cpp_code def create_cpp_namespace(owner): return {'%s_values' % self.name: self.values.astype(np.double, order='C', copy=False).ravel()} self.implementations.add_dynamic_implementation('cpp', code=create_cpp_implementation, namespace=create_cpp_namespace, name=self.name) def create_cython_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) code = ''' cdef double %NAME%(const double t, const int i): global _namespace%NAME%_values cdef double epsilon = %DT% / %K% if i < 0 or i >= %COLS%: return _numpy.nan cdef int timestep = (int)((t/epsilon + 0.5)/%K%) if timestep < 0: timestep = 0 elif timestep >= %ROWS%: timestep = %ROWS%-1 return _namespace%NAME%_values[timestep*%COLS% + i] ''' code = replace(code, {'%NAME%': self.name, '%DT%': '%.18f' % dt, '%K%': str(K), '%COLS%': str(self.values.shape[1]), '%ROWS%': str(self.values.shape[0])}) return code def create_cython_namespace(owner): return {'%s_values' % self.name: self.values.astype(np.double, order='C', copy=False).ravel()} self.implementations.add_dynamic_implementation('cython', code=create_cython_implementation, namespace=create_cython_namespace, name=self.name)
def create_runner_codeobj(group, code, template_name, variable_indices=None, name=None, check_units=True, needed_variables=None, additional_variables=None, additional_namespace=None, template_kwds=None): ''' Create a `CodeObject` for the execution of code in the context of a `Group`. Parameters ---------- group : `Group` The group where the code is to be run code : str The code to be executed. template_name : str The name of the template to use for the code. variable_indices : dict-like, optional A mapping from `Variable` objects to index names (strings). If none is given, uses the corresponding attribute of `group`. name : str, optional A name for this code object, will use ``group + '_codeobject*'`` if none is given. check_units : bool, optional Whether to check units in the statement. Defaults to ``True``. needed_variables: list of str, optional A list of variables that are neither present in the abstract code, nor in the ``USES_VARIABLES`` statement in the template. This is only rarely necessary, an example being a `StateMonitor` where the names of the variables are neither known to the template nor included in the abstract code statements. additional_variables : dict-like, optional A mapping of names to `Variable` objects, used in addition to the variables saved in `group`. additional_namespace : dict-like, optional A mapping from names to objects, used in addition to the namespace saved in `group`. template_kwds : dict, optional A dictionary of additional information that is passed to the template. ''' logger.debug('Creating code object for abstract code:\n' + str(code)) from brian2.devices import get_device device = get_device() if check_units: if isinstance(code, dict): for c in code.values(): check_code_units(c, group, additional_variables=additional_variables, additional_namespace=additional_namespace) else: check_code_units(code, group, additional_variables=additional_variables, additional_namespace=additional_namespace) codeobj_class = device.code_object_class(group.codeobj_class) template = getattr(codeobj_class.templater, template_name) all_variables = dict(group.variables) if additional_variables is not None: all_variables.update(additional_variables) # Determine the identifiers that were used if isinstance(code, dict): used_known = set() unknown = set() for v in code.values(): _, uk, u = analyse_identifiers(v, all_variables, recursive=True) used_known |= uk unknown |= u else: _, used_known, unknown = analyse_identifiers(code, all_variables, recursive=True) logger.debug('Unknown identifiers in the abstract code: ' + str(unknown)) # Only pass the variables that are actually used variables = {} for var in used_known: # Emit a warning if a variable is also present in the namespace if (var in group.namespace or (additional_namespace is not None and var in additional_namespace[1])): message = ('Variable {var} is present in the namespace but is also an' ' internal variable of {name}, the internal variable will' ' be used.'.format(var=var, name=group.name)) logger.warn(message, 'create_runner_codeobj.resolution_conflict', once=True) variables[var] = all_variables[var] resolved_namespace = group.namespace.resolve_all(unknown, additional_namespace) for varname, value in resolved_namespace.iteritems(): if isinstance(value, Function): variables[varname] = value else: unit = get_unit(value) # For the moment, only allow the use of scalar values array_value = np.asarray(value) if array_value.shape != (): raise TypeError('Name "%s" does not refer to a scalar value' % varname) variables[varname] = Constant(name=varname, unit=unit, value=value) # Add variables that are not in the abstract code, nor specified in the # template but nevertheless necessary if needed_variables is None: needed_variables = [] for var in needed_variables: variables[var] = all_variables[var] # Also add the variables that the template needs for var in template.variables: try: variables[var] = all_variables[var] except KeyError as ex: # We abuse template.variables here to also store names of things # from the namespace (e.g. rand) that are needed # TODO: Improve all of this namespace/specifier handling if group is not None: # Try to find the name in the group's namespace variables[var] = group.namespace.resolve(var, additional_namespace) else: raise ex # always add N, the number of neurons or synapses variables['N'] = all_variables['N'] if name is None: if group is not None: name = '%s_%s_codeobject*' % (group.name, template_name) else: name = '%s_codeobject*' % template_name all_variable_indices = copy.copy(group.variables.indices) if additional_variables is not None: all_variable_indices.update(additional_variables.indices) if variable_indices is not None: all_variable_indices.update(variable_indices) # Add the indices needed by the variables varnames = variables.keys() for varname in varnames: var_index = all_variable_indices[varname] if var_index != '_idx': variables[var_index] = all_variables[var_index] return device.code_object(owner=group, name=name, abstract_code=code, variables=variables, template_name=template_name, variable_indices=all_variable_indices, template_kwds=template_kwds, codeobj_class=group.codeobj_class)
def __init__(self, values, dt, name=None): if name is None: name = '_timedarray*' Nameable.__init__(self, name) unit = get_unit(values) values = np.asarray(values) self.values = values dt = float(dt) self.dt = dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(t=second, result=unit) def timed_array_func(t): i = np.clip(np.int_(np.float_(t) / dt + 0.5), 0, len(values)-1) return values[i] * unit Function.__init__(self, pyfunc=timed_array_func) # we use dynamic implementations because we want to do upsampling # in a way that avoids rounding problems with the group's dt def create_numpy_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) epsilon = dt / K n_values = len(values) def unitless_timed_array_func(t): timestep = np.clip(np.int_(np.round(t/epsilon)) / K, 0, n_values-1) return values[timestep] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit return unitless_timed_array_func self.implementations.add_dynamic_implementation('numpy', create_numpy_implementation) def create_cpp_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) cpp_code = {'support_code': ''' inline double _timedarray_%NAME%(const double t, const int _num_values, const double* _values) { const double epsilon = %DT% / %K%; int i = (int)((t/epsilon + 0.5)/%K%); // rounds to nearest int for positive values if(i<0) i = 0; if(i>=_num_values) i = _num_values-1; return _values[i]; } '''.replace('%NAME%', self.name).replace('%DT%', '%.18f' % dt).replace('%K%', str(K)), 'hashdefine_code': ''' #define %NAME%(t) _timedarray_%NAME%(t, _%NAME%_num_values, _%NAME%_values) '''.replace('%NAME%', self.name)} return cpp_code def create_cpp_namespace(owner): return {'_%s_num_values' % self.name: len(self.values), '_%s_values' % self.name: self.values} self.implementations.add_dynamic_implementation('cpp', create_cpp_implementation, create_cpp_namespace, name=self.name)
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 parse_expression_dimensions(expr, variables): ''' Returns the unit value of an expression, and checks its validity Parameters ---------- expr : str The expression to check. variables : dict Dictionary of all variables used in the `expr` (including `Constant` objects for external variables) Returns ------- unit : Quantity The output unit of the expression Raises ------ SyntaxError If the expression cannot be parsed, or if it uses ``a**b`` for ``b`` anything other than a constant number. DimensionMismatchError If any part of the expression is dimensionally inconsistent. ''' # If we are working on a string, convert to the top level node if isinstance(expr, basestring): mod = ast.parse(expr, mode='eval') expr = mod.body if expr.__class__ is getattr(ast, 'NameConstant', None): # new class for True, False, None in Python 3.4 value = expr.value if value is True or value is False: return DIMENSIONLESS else: raise ValueError('Do not know how to handle value %s' % value) if expr.__class__ is ast.Name: name = expr.id # Raise an error if a function is called as if it were a variable # (most of the time this happens for a TimedArray) if name in variables and isinstance(variables[name], Function): raise SyntaxError('%s was used like a variable/constant, but it is ' 'a function.' % name) if name in variables: return variables[name].dim elif name in ['True', 'False']: return DIMENSIONLESS else: raise KeyError('Unknown identifier %s' % name) elif expr.__class__ is ast.Num: return DIMENSIONLESS elif expr.__class__ is ast.BoolOp: # check that the units are valid in each subexpression for node in expr.values: parse_expression_dimensions(node, variables) # but the result is a bool, so we just return 1 as the unit return DIMENSIONLESS elif expr.__class__ is ast.Compare: # check that the units are consistent in each subexpression subexprs = [expr.left]+expr.comparators subunits = [] for node in subexprs: subunits.append(parse_expression_dimensions(node, variables)) for left_dim, right_dim in zip(subunits[:-1], subunits[1:]): if not have_same_dimensions(left_dim, right_dim): msg = ('Comparison of expressions with different units. Expression ' '"{}" has unit ({}), while expression "{}" has units ({})').format( NodeRenderer().render_node(expr.left), get_dimensions(left_dim), NodeRenderer().render_node(expr.comparators[0]), get_dimensions(right_dim)) raise DimensionMismatchError(msg) # but the result is a bool, so we just return 1 as the unit return DIMENSIONLESS elif expr.__class__ is ast.Call: if len(expr.keywords): raise ValueError("Keyword arguments not supported.") elif getattr(expr, 'starargs', None) is not None: raise ValueError("Variable number of arguments not supported") elif getattr(expr, 'kwargs', None) is not None: raise ValueError("Keyword arguments not supported") func = variables.get(expr.func.id, None) if func is None: raise SyntaxError('Unknown function %s' % expr.func.id) if not hasattr(func, '_arg_units') or not hasattr(func, '_return_unit'): raise ValueError(('Function %s does not specify how it ' 'deals with units.') % expr.func.id) if len(func._arg_units) != len(expr.args): raise SyntaxError('Function %s was called with %d parameters, ' 'needs %d.' % (expr.func.id, len(expr.args), len(func._arg_units))) for idx, (arg, expected_unit) in enumerate(zip(expr.args, func._arg_units)): # A "None" in func._arg_units means: No matter what unit if expected_unit is None: continue elif expected_unit == bool: if not is_boolean_expression(arg, variables): raise TypeError(('Argument number %d for function %s was ' 'expected to be a boolean value, but is ' '"%s".') % (idx + 1, expr.func.id, NodeRenderer().render_node(arg))) else: arg_unit = parse_expression_dimensions(arg, variables) if not have_same_dimensions(arg_unit, expected_unit): msg = ('Argument number {} for function {} does not have the ' 'correct units. Expression "{}" has units ({}), but ' 'should be ({}).').format( idx+1, expr.func.id, NodeRenderer().render_node(arg), get_dimensions(arg_unit), get_dimensions(expected_unit)) raise DimensionMismatchError(msg) if func._return_unit == bool: return DIMENSIONLESS elif isinstance(func._return_unit, (Unit, int)): # Function always returns the same unit return getattr(func._return_unit, 'dim', DIMENSIONLESS) else: # Function returns a unit that depends on the arguments arg_units = [parse_expression_dimensions(arg, variables) for arg in expr.args] return func._return_unit(*arg_units).dim elif expr.__class__ is ast.BinOp: op = expr.op.__class__.__name__ left_dim = parse_expression_dimensions(expr.left, variables) right_dim = parse_expression_dimensions(expr.right, variables) if op in ['Add', 'Sub', 'Mod']: # dimensions should be the same if left_dim is not right_dim: op_symbol = {'Add': '+', 'Sub': '-', 'Mod': '%'}.get(op) left_str = NodeRenderer().render_node(expr.left) right_str = NodeRenderer().render_node(expr.right) left_unit = repr(get_unit(left_dim)) right_unit = repr(get_unit(right_dim)) error_msg = ('Expression "{left} {op} {right}" uses ' 'inconsistent units ("{left}" has unit ' '{left_unit}; "{right}" ' 'has unit {right_unit})').format(left=left_str, right=right_str, op=op_symbol, left_unit=left_unit, right_unit=right_unit) raise DimensionMismatchError(error_msg) u = left_dim elif op == 'Mult': u = left_dim*right_dim elif op == 'Div': u = left_dim/right_dim elif op == 'FloorDiv': if not (left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS): raise SyntaxError('Floor division can only be used on ' 'dimensionless values.') u = DIMENSIONLESS elif op == 'Pow': if left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS: return DIMENSIONLESS n = _get_value_from_expression(expr.right, variables) u = left_dim**n else: raise SyntaxError("Unsupported operation "+op) return u elif expr.__class__ is ast.UnaryOp: op = expr.op.__class__.__name__ # check validity of operand and get its unit u = parse_expression_dimensions(expr.operand, variables) if op == 'Not': return DIMENSIONLESS else: return u else: raise SyntaxError('Unsupported operation ' + str(expr.__class__))
def __setattr__(self, name, val, level=0): # attribute access is switched off until this attribute is created by # _enable_group_attributes if not hasattr(self, '_group_attribute_access_active') or name in self.__dict__: object.__setattr__(self, name, val) elif (name in self.__getattribute__('__dict__') or name in self.__getattribute__('__class__').__dict__): # Makes sure that classes can override the "variables" mechanism # with instance/class attributes and properties return object.__setattr__(self, name, val) elif name in self.variables: var = self.variables[name] if not isinstance(val, basestring): if var.dim is DIMENSIONLESS: fail_for_dimension_mismatch(val, var.dim, ('%s should be set with a ' 'dimensionless value, but got ' '{value}') % name, value=val) else: fail_for_dimension_mismatch(val, var.dim, ('%s should be set with a ' 'value with units %r, but got ' '{value}') % (name, get_unit(var.dim)), value=val) if var.read_only: raise TypeError('Variable %s is read-only.' % name) # Make the call X.var = ... equivalent to X.var[:] = ... var.get_addressable_value_with_unit(name, self).set_item(slice(None), val, level=level+1) elif len(name) and name[-1]=='_' and name[:-1] in self.variables: # no unit checking var = self.variables[name[:-1]] if var.read_only: raise TypeError('Variable %s is read-only.' % name[:-1]) # Make the call X.var = ... equivalent to X.var[:] = ... var.get_addressable_value(name[:-1], self).set_item(slice(None), val, level=level+1) elif hasattr(self, name) or name.startswith('_'): object.__setattr__(self, name, val) else: # Try to suggest the correct name in case of a typo checker = SpellChecker([varname for varname, var in self.variables.iteritems() if not (varname.startswith('_') or var.read_only)]) if name.endswith('_'): suffix = '_' name = name[:-1] else: suffix = '' error_msg = 'Could not find a state variable with name "%s".' % name suggestions = checker.suggest(name) if len(suggestions) == 1: suggestion, = suggestions error_msg += ' Did you mean to write "%s%s"?' % (suggestion, suffix) elif len(suggestions) > 1: error_msg += (' Did you mean to write any of the following: %s ?' % (', '.join(['"%s%s"' % (suggestion, suffix) for suggestion in suggestions]))) error_msg += (' Use the add_attribute method if you intend to add ' 'a new attribute to the object.') raise AttributeError(error_msg)
class SingleEquation(collections.Hashable, CacheKey): ''' Class for internal use, encapsulates a single equation or parameter. .. note:: This class should never be used directly, it is only useful as part of the `Equations` class. Parameters ---------- type : {PARAMETER, DIFFERENTIAL_EQUATION, SUBEXPRESSION} The type of the equation. varname : str The variable that is defined by this equation. dimensions : `Dimension` The physical dimensions of the variable var_type : {FLOAT, INTEGER, BOOLEAN} The type of the variable (floating point value or boolean). expr : `Expression`, optional The expression defining the variable (or ``None`` for parameters). flags: list of str, optional A list of flags that give additional information about this equation. What flags are possible depends on the type of the equation and the context. ''' _cache_irrelevant_attributes = {'update_order'} def __init__(self, type, varname, dimensions, var_type=FLOAT, expr=None, flags=None): self.type = type self.varname = varname self.dim = get_dimensions(dimensions) self.var_type = var_type if dimensions is not DIMENSIONLESS: if var_type == BOOLEAN: raise TypeError( 'Boolean variables are necessarily dimensionless.') elif var_type == INTEGER: raise TypeError( 'Integer variables are necessarily dimensionless.') if type == DIFFERENTIAL_EQUATION: if var_type != FLOAT: raise TypeError( 'Differential equations can only define floating point variables' ) self.expr = expr if flags is None: self.flags = [] else: self.flags = flags # will be set later in the sort_subexpressions method of Equations self.update_order = -1 unit = property(lambda self: get_unit(self.dim), doc='The `Unit` of this equation.') identifiers = property(lambda self: self.expr.identifiers if not self.expr is None else set([]), doc='All identifiers in the RHS of this equation.') stochastic_variables = property( lambda self: set([ variable for variable in self.identifiers if variable == 'xi' or variable.startswith('xi_') ]), doc='Stochastic variables in the RHS of this equation') def __eq__(self, other): if not isinstance(other, SingleEquation): return NotImplemented return self._state_tuple == other._state_tuple def __ne__(self, other): return not self == other def __hash__(self): return hash(self._state_tuple) def _latex(self, *args): if self.type == DIFFERENTIAL_EQUATION: return (r'\frac{\mathrm{d}' + sympy.latex(self.varname) + r'}{\mathrm{d}t} = ' + sympy.latex(str_to_sympy(self.expr.code))) elif self.type == SUBEXPRESSION: return (sympy.latex(self.varname) + ' = ' + sympy.latex(str_to_sympy(self.expr.code))) elif self.type == PARAMETER: return sympy.latex(self.varname) def __str__(self): if self.type == DIFFERENTIAL_EQUATION: s = 'd' + self.varname + '/dt' else: s = self.varname if not self.expr is None: s += ' = ' + str(self.expr) s += ' : ' + get_unit_for_display(self.dim) if len(self.flags): s += ' (' + ', '.join(self.flags) + ')' return s def __repr__(self): s = '<' + self.type + ' ' + self.varname if not self.expr is None: s += ': ' + self.expr.code s += ' (Unit: ' + get_unit_for_display(self.dim) if len(self.flags): s += ', flags: ' + ', '.join(self.flags) s += ')>' return s def _repr_pretty_(self, p, cycle): ''' Pretty printing for ipython. ''' if cycle: # should never happen raise AssertionError( 'Cyclical call of SingleEquation._repr_pretty') if self.type == DIFFERENTIAL_EQUATION: p.text('d' + self.varname + '/dt') else: p.text(self.varname) if not self.expr is None: p.text(' = ') p.pretty(self.expr) p.text(' : ') p.pretty(get_unit(self.dim)) if len(self.flags): p.text(' (' + ', '.join(self.flags) + ')') def _repr_latex_(self): return '$' + sympy.latex(self) + '$'
def dimensions_and_type_from_string(unit_string): ''' Returns the physical dimensions that results from evaluating a string like "siemens / metre ** 2", allowing for the special string "1" to signify dimensionless units, the string "boolean" for a boolean and "integer" for an integer variable. Parameters ---------- unit_string : str The string that should evaluate to a unit Returns ------- d, type : (`Dimension`, {FLOAT, INTEGER or BOOL}) The resulting physical dimensions and the type of the variable. Raises ------ ValueError If the string cannot be evaluated to a unit. ''' # Lazy import to avoid circular dependency from brian2.core.namespace import DEFAULT_UNITS global _base_units # we only want to do this once global _single_base_units if _base_units is None: base_units_for_dims = {} _base_units = collections.OrderedDict() for unit_name, unit in DEFAULT_UNITS.iteritems(): if float(unit) == 1.0: _base_units[unit_name] = unit # Go through it a second time -- we only want to display one unit per # dimensionality to the user and don't bother displaying powered units # (meter2, meter3, ...) for unit in _base_units.itervalues(): if (unit.dim not in base_units_for_dims and repr(unit)[-1] not in ['2', '3']): base_units_for_dims[unit.dim] = unit _single_base_units = sorted( [repr(unit) for unit in base_units_for_dims.itervalues()]) unit_string = unit_string.strip() # Special case: dimensionless unit if unit_string == '1': return DIMENSIONLESS, FLOAT # Another special case: boolean variable if unit_string == 'boolean': return DIMENSIONLESS, BOOLEAN if unit_string == 'bool': raise TypeError("Use 'boolean' not 'bool' as the unit for a boolean " "variable.") # Yet another special case: integer variable if unit_string == 'integer': return DIMENSIONLESS, INTEGER # Check first whether the expression only refers to base units identifiers = get_identifiers(unit_string) for identifier in identifiers: if identifier not in _base_units: if identifier in DEFAULT_UNITS: # A known unit, but not a base unit base_unit = get_unit(DEFAULT_UNITS[identifier].dim) if not repr(base_unit) in _base_units: # Make sure that we don't suggest a unit that is not allowed # (should not happen, normally) base_unit = Unit(1, dim=base_unit.dim) raise ValueError( ('Unit specification refers to ' '"{identifier}", but this is not a base ' 'unit. Use "{base_unit}" ' 'instead.').format(identifier=identifier, base_unit=repr(base_unit))) else: # Not a known unit allowed = ', '.join(_single_base_units) raise ValueError(('Unit specification refers to ' '"{identifier}", but this is not a base ' 'unit. The following base units are ' 'allowed: {allowed_units} (plus some ' 'variants of these, e.g. "Hz" instead of ' '"hertz", or "meter" instead of ' '"metre").').format(identifier=identifier, allowed_units=allowed)) try: evaluated_unit = eval(unit_string, _base_units) except Exception as ex: raise ValueError(('Could not interpret "%s" as a unit specification: ' '%s') % (unit_string, ex)) # Check whether the result is a unit if not isinstance(evaluated_unit, Unit): if isinstance(evaluated_unit, Quantity): raise ValueError( ('"%s" does not evaluate to a unit but to a ' 'quantity -- make sure to only use units, e.g. ' '"siemens/metre**2" and not "1 * siemens/metre**2"') % unit_string) else: raise ValueError( ('"%s" does not evaluate to a unit, the result ' 'has type %s instead.' % (unit_string, type(evaluated_unit)))) # No error has been raised, all good return evaluated_unit.dim, FLOAT
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: matches.append((description, namespace[identifier])) 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) # 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
def parse_expression_dimensions(expr, variables): ''' Returns the unit value of an expression, and checks its validity Parameters ---------- expr : str The expression to check. variables : dict Dictionary of all variables used in the `expr` (including `Constant` objects for external variables) Returns ------- unit : Quantity The output unit of the expression Raises ------ SyntaxError If the expression cannot be parsed, or if it uses ``a**b`` for ``b`` anything other than a constant number. DimensionMismatchError If any part of the expression is dimensionally inconsistent. ''' # If we are working on a string, convert to the top level node if isinstance(expr, basestring): mod = ast.parse(expr, mode='eval') expr = mod.body if expr.__class__ is getattr(ast, 'NameConstant', None): # new class for True, False, None in Python 3.4 value = expr.value if value is True or value is False: return DIMENSIONLESS else: raise ValueError('Do not know how to handle value %s' % value) if expr.__class__ is ast.Name: name = expr.id # Raise an error if a function is called as if it were a variable # (most of the time this happens for a TimedArray) if name in variables and isinstance(variables[name], Function): raise SyntaxError( '%s was used like a variable/constant, but it is ' 'a function.' % name) if name in variables: return variables[name].dim elif name in ['True', 'False']: return DIMENSIONLESS else: raise KeyError('Unknown identifier %s' % name) elif expr.__class__ is ast.Num: return DIMENSIONLESS elif expr.__class__ is ast.BoolOp: # check that the units are valid in each subexpression for node in expr.values: parse_expression_dimensions(node, variables) # but the result is a bool, so we just return 1 as the unit return DIMENSIONLESS elif expr.__class__ is ast.Compare: # check that the units are consistent in each subexpression subexprs = [expr.left] + expr.comparators subunits = [] for node in subexprs: subunits.append(parse_expression_dimensions(node, variables)) for left_dim, right_dim in zip(subunits[:-1], subunits[1:]): if not have_same_dimensions(left_dim, right_dim): msg = ( 'Comparison of expressions with different units. Expression ' '"{}" has unit ({}), while expression "{}" has units ({})' ).format(NodeRenderer().render_node(expr.left), get_dimensions(left_dim), NodeRenderer().render_node(expr.comparators[0]), get_dimensions(right_dim)) raise DimensionMismatchError(msg) # but the result is a bool, so we just return 1 as the unit return DIMENSIONLESS elif expr.__class__ is ast.Call: if len(expr.keywords): raise ValueError("Keyword arguments not supported.") elif getattr(expr, 'starargs', None) is not None: raise ValueError("Variable number of arguments not supported") elif getattr(expr, 'kwargs', None) is not None: raise ValueError("Keyword arguments not supported") func = variables.get(expr.func.id, None) if func is None: raise SyntaxError('Unknown function %s' % expr.func.id) if not hasattr(func, '_arg_units') or not hasattr( func, '_return_unit'): raise ValueError(('Function %s does not specify how it ' 'deals with units.') % expr.func.id) if len(func._arg_units) != len(expr.args): raise SyntaxError( 'Function %s was called with %d parameters, ' 'needs %d.' % (expr.func.id, len(expr.args), len(func._arg_units))) for idx, (arg, expected_unit) in enumerate(zip(expr.args, func._arg_units)): # A "None" in func._arg_units means: No matter what unit if expected_unit is None: continue elif expected_unit == bool: if not is_boolean_expression(arg, variables): raise TypeError( ('Argument number %d for function %s was ' 'expected to be a boolean value, but is ' '"%s".') % (idx + 1, expr.func.id, NodeRenderer().render_node(arg))) else: arg_unit = parse_expression_dimensions(arg, variables) if not have_same_dimensions(arg_unit, expected_unit): msg = ( 'Argument number {} for function {} does not have the ' 'correct units. Expression "{}" has units ({}), but ' 'should be ({}).').format( idx + 1, expr.func.id, NodeRenderer().render_node(arg), get_dimensions(arg_unit), get_dimensions(expected_unit)) raise DimensionMismatchError(msg) if func._return_unit == bool: return DIMENSIONLESS elif isinstance(func._return_unit, (Unit, int)): # Function always returns the same unit return getattr(func._return_unit, 'dim', DIMENSIONLESS) else: # Function returns a unit that depends on the arguments arg_units = [ parse_expression_dimensions(arg, variables) for arg in expr.args ] return func._return_unit(*arg_units).dim elif expr.__class__ is ast.BinOp: op = expr.op.__class__.__name__ left_dim = parse_expression_dimensions(expr.left, variables) right_dim = parse_expression_dimensions(expr.right, variables) if op in ['Add', 'Sub', 'Mod']: # dimensions should be the same if left_dim is not right_dim: op_symbol = {'Add': '+', 'Sub': '-', 'Mod': '%'}.get(op) left_str = NodeRenderer().render_node(expr.left) right_str = NodeRenderer().render_node(expr.right) left_unit = repr(get_unit(left_dim)) right_unit = repr(get_unit(right_dim)) error_msg = ('Expression "{left} {op} {right}" uses ' 'inconsistent units ("{left}" has unit ' '{left_unit}; "{right}" ' 'has unit {right_unit})').format( left=left_str, right=right_str, op=op_symbol, left_unit=left_unit, right_unit=right_unit) raise DimensionMismatchError(error_msg) u = left_dim elif op == 'Mult': u = left_dim * right_dim elif op == 'Div': u = left_dim / right_dim elif op == 'FloorDiv': if not (left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS): raise SyntaxError('Floor division can only be used on ' 'dimensionless values.') u = DIMENSIONLESS elif op == 'Pow': if left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS: return DIMENSIONLESS n = _get_value_from_expression(expr.right, variables) u = left_dim**n else: raise SyntaxError("Unsupported operation " + op) return u elif expr.__class__ is ast.UnaryOp: op = expr.op.__class__.__name__ # check validity of operand and get its unit u = parse_expression_dimensions(expr.operand, variables) if op == 'Not': return DIMENSIONLESS else: return u else: raise SyntaxError('Unsupported operation ' + str(expr.__class__))
def check_units_statements(code, variables): ''' Check the units for a series of statements. Setting a model variable has to use the correct unit. For newly introduced temporary variables, the unit is determined and used to check the following statements to ensure consistency. Parameters ---------- code : str The statements as a (multi-line) string variables : dict of `Variable` objects The information about all variables used in `code` (including `Constant` objects for external variables) Raises ------ KeyError In case on of the identifiers cannot be resolved. DimensionMismatchError If an unit mismatch occurs during the evaluation. ''' variables = dict(variables) # Avoid a circular import from brian2.codegen.translation import analyse_identifiers known = set(variables.keys()) newly_defined, _, unknown = analyse_identifiers(code, known) if len(unknown): raise AssertionError( ('Encountered unknown identifiers, this should ' 'not happen at this stage. Unkown identifiers: %s' % unknown)) code = re.split(r'[;\n]', code) for line in code: line = line.strip() if not len(line): continue # skip empty lines varname, op, expr, comment = parse_statement(line) if op in ('+=', '-=', '*=', '/=', '%='): # Replace statements such as "w *=2" by "w = w * 2" expr = '{var} {op_first} {expr}'.format(var=varname, op_first=op[0], expr=expr) op = '=' elif op == '=': pass else: raise AssertionError('Unknown operator "%s"' % op) expr_unit = parse_expression_unit(expr, variables) if varname in variables: expected_unit = variables[varname].unit fail_for_dimension_mismatch(expr_unit, expected_unit, ('The right-hand-side of code ' 'statement ""%s" does not have the ' 'expected unit %r') % (line, expected_unit)) elif varname in newly_defined: # note the unit for later variables[varname] = Variable(name=varname, unit=get_unit(expr_unit), scalar=False) else: raise AssertionError(('Variable "%s" is neither in the variables ' 'dictionary nor in the list of undefined ' 'variables.' % varname))
def _init_1d(self): dimensions = self.dim unit = get_unit(dimensions) values = self.values dt = self.dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(t=second, result=unit) def timed_array_func(t): # We round according to the current defaultclock.dt K = _find_K(float(defaultclock.dt), dt) epsilon = dt / K i = np.clip(np.int_(np.round(np.asarray(t/epsilon)) / K), 0, len(values)-1) return Quantity(values[i], dim=dimensions) Function.__init__(self, pyfunc=timed_array_func) # we use dynamic implementations because we want to do upsampling # in a way that avoids rounding problems with the group's dt def create_numpy_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) n_values = len(values) epsilon = dt / K def unitless_timed_array_func(t): timestep = np.clip(np.int_(np.round(t/epsilon) / K), 0, n_values-1) return values[timestep] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit return unitless_timed_array_func self.implementations.add_dynamic_implementation('numpy', create_numpy_implementation) def create_cpp_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) support_code = ''' static inline double %NAME%(const double t) { const double epsilon = %DT% / %K%; int i = (int)((t/epsilon + 0.5)/%K%); if(i < 0) i = 0; if(i >= %NUM_VALUES%) i = %NUM_VALUES%-1; return _namespace%NAME%_values[i]; } '''.replace('%NAME%', self.name).replace('%DT%', '%.18f' % dt).replace('%K%', str(K)).replace('%NUM_VALUES%', str(len(self.values))) cpp_code = {'support_code': support_code} return cpp_code def create_cpp_namespace(owner): return {'%s_values' % self.name: self.values} self.implementations.add_dynamic_implementation('cpp', code=create_cpp_implementation, namespace=create_cpp_namespace, name=self.name) def create_cython_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) code = ''' cdef double %NAME%(const double t): global _namespace%NAME%_values cdef double epsilon = %DT% / %K% cdef int i = (int)((t/epsilon + 0.5)/%K%) if i < 0: i = 0 if i >= %NUM_VALUES%: i = %NUM_VALUES% - 1 return _namespace%NAME%_values[i] '''.replace('%NAME%', self.name).replace('%DT%', '%.18f' % dt).replace('%K%', str(K)).replace('%NUM_VALUES%', str(len(self.values))) return code def create_cython_namespace(owner): return {'%s_values' % self.name: self.values} self.implementations.add_dynamic_implementation('cython', code=create_cython_implementation, namespace=create_cython_namespace, name=self.name)
def __setattr__(self, name, val, level=0): # attribute access is switched off until this attribute is created by # _enable_group_attributes if not hasattr( self, '_group_attribute_access_active') or name in self.__dict__: object.__setattr__(self, name, val) elif (name in self.__getattribute__('__dict__') or name in self.__getattribute__('__class__').__dict__): # Makes sure that classes can override the "variables" mechanism # with instance/class attributes and properties return object.__setattr__(self, name, val) elif name in self.variables: var = self.variables[name] if not isinstance(val, basestring): if var.dim is DIMENSIONLESS: fail_for_dimension_mismatch( val, var.dim, ('%s should be set with a ' 'dimensionless value, but got ' '{value}') % name, value=val) else: fail_for_dimension_mismatch( val, var.dim, ('%s should be set with a ' 'value with units %r, but got ' '{value}') % (name, get_unit(var.dim)), value=val) if var.read_only: raise TypeError('Variable %s is read-only.' % name) # Make the call X.var = ... equivalent to X.var[:] = ... var.get_addressable_value_with_unit(name, self).set_item(slice(None), val, level=level + 1) elif len(name) and name[-1] == '_' and name[:-1] in self.variables: # no unit checking var = self.variables[name[:-1]] if var.read_only: raise TypeError('Variable %s is read-only.' % name[:-1]) # Make the call X.var = ... equivalent to X.var[:] = ... var.get_addressable_value(name[:-1], self).set_item(slice(None), val, level=level + 1) elif hasattr(self, name) or name.startswith('_'): object.__setattr__(self, name, val) else: # Try to suggest the correct name in case of a typo checker = SpellChecker([ varname for varname, var in self.variables.items() if not (varname.startswith('_') or var.read_only) ]) if name.endswith('_'): suffix = '_' name = name[:-1] else: suffix = '' error_msg = 'Could not find a state variable with name "%s".' % name suggestions = checker.suggest(name) if len(suggestions) == 1: suggestion, = suggestions error_msg += ' Did you mean to write "%s%s"?' % (suggestion, suffix) elif len(suggestions) > 1: error_msg += ( ' Did you mean to write any of the following: %s ?' % (', '.join([ '"%s%s"' % (suggestion, suffix) for suggestion in suggestions ]))) error_msg += (' Use the add_attribute method if you intend to add ' 'a new attribute to the object.') raise AttributeError(error_msg)
def __init__(self, values, dt, name=None): if name is None: name = '_timedarray*' Nameable.__init__(self, name) unit = get_unit(values) values = np.asarray(values) self.values = values dt = float(dt) self.dt = dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(t=second, result=unit) def timed_array_func(t): i = np.clip(np.int_(np.float_(t) / dt + 0.5), 0, len(values) - 1) return values[i] * unit Function.__init__(self, pyfunc=timed_array_func) # we use dynamic implementations because we want to do upsampling # in a way that avoids rounding problems with the group's dt def create_numpy_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) epsilon = dt / K n_values = len(values) def unitless_timed_array_func(t): timestep = np.clip( np.int_(np.round(t / epsilon)) / K, 0, n_values - 1) return values[timestep] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit return unitless_timed_array_func self.implementations.add_dynamic_implementation( 'numpy', create_numpy_implementation) def create_cpp_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) cpp_code = { 'support_code': ''' inline double _timedarray_%NAME%(const double t, const int _num_values, const double* _values) { const double epsilon = %DT% / %K%; int i = (int)((t/epsilon + 0.5)/%K%); // rounds to nearest int for positive values if(i<0) i = 0; if(i>=_num_values) i = _num_values-1; return _values[i]; } '''.replace('%NAME%', self.name).replace('%DT%', '%.18f' % dt).replace( '%K%', str(K)), 'hashdefine_code': ''' #define %NAME%(t) _timedarray_%NAME%(t, _%NAME%_num_values, _%NAME%_values) '''.replace('%NAME%', self.name) } return cpp_code def create_cpp_namespace(owner): return { '_%s_num_values' % self.name: len(self.values), '_%s_values' % self.name: self.values } self.implementations.add_dynamic_implementation( 'cpp', create_cpp_implementation, create_cpp_namespace, name=self.name)
def check_units_statements(code, variables): ''' Check the units for a series of statements. Setting a model variable has to use the correct unit. For newly introduced temporary variables, the unit is determined and used to check the following statements to ensure consistency. Parameters ---------- code : str The statements as a (multi-line) string variables : dict of `Variable` objects The information about all variables used in `code` (including `Constant` objects for external variables) Raises ------ KeyError In case on of the identifiers cannot be resolved. DimensionMismatchError If an unit mismatch occurs during the evaluation. ''' variables = dict(variables) # Avoid a circular import from brian2.codegen.translation import analyse_identifiers known = set(variables.keys()) newly_defined, _, unknown = analyse_identifiers(code, known) if len(unknown): raise AssertionError(('Encountered unknown identifiers, this should ' 'not happen at this stage. Unkown identifiers: %s' % unknown)) code = re.split(r'[;\n]', code) for line in code: line = line.strip() if not len(line): continue # skip empty lines varname, op, expr, comment = parse_statement(line) if op in ('+=', '-=', '*=', '/=', '%='): # Replace statements such as "w *=2" by "w = w * 2" expr = '{var} {op_first} {expr}'.format(var=varname, op_first=op[0], expr=expr) op = '=' elif op == '=': pass else: raise AssertionError('Unknown operator "%s"' % op) expr_unit = parse_expression_unit(expr, variables) if varname in variables: expected_unit = variables[varname].unit fail_for_dimension_mismatch(expr_unit, expected_unit, ('The right-hand-side of code ' 'statement ""%s" does not have the ' 'expected unit %r') % (line, expected_unit)) elif varname in newly_defined: # note the unit for later variables[varname] = Variable(name=varname, unit=get_unit(expr_unit), scalar=False) else: raise AssertionError(('Variable "%s" is neither in the variables ' 'dictionary nor in the list of undefined ' 'variables.' % varname))
def dimensions_and_type_from_string(unit_string): ''' Returns the physical dimensions that results from evaluating a string like "siemens / metre ** 2", allowing for the special string "1" to signify dimensionless units, the string "boolean" for a boolean and "integer" for an integer variable. Parameters ---------- unit_string : str The string that should evaluate to a unit Returns ------- d, type : (`Dimension`, {FLOAT, INTEGER or BOOL}) The resulting physical dimensions and the type of the variable. Raises ------ ValueError If the string cannot be evaluated to a unit. ''' # Lazy import to avoid circular dependency from brian2.core.namespace import DEFAULT_UNITS global _base_units_with_alternatives global _base_units if _base_units_with_alternatives is None: base_units_for_dims = {} for unit_name, unit in reversed(DEFAULT_UNITS.items()): if float(unit) == 1.0 and repr(unit)[-1] not in ['2', '3']: if unit.dim in base_units_for_dims: if unit_name not in base_units_for_dims[unit.dim]: base_units_for_dims[unit.dim].append(unit_name) else: base_units_for_dims[unit.dim] = [repr(unit)] if unit_name != repr(unit): base_units_for_dims[unit.dim].append(unit_name) alternatives = sorted( [tuple(values) for values in base_units_for_dims.itervalues()]) _base_units = dict([(v, DEFAULT_UNITS[v]) for values in alternatives for v in values]) # Create a string that lists all allowed base units alternative_strings = [] for units in alternatives: string = units[0] if len(units) > 1: string += ' ({other_units})'.format( other_units=', '.join(units[1:])) alternative_strings.append(string) _base_units_with_alternatives = ', '.join(alternative_strings) unit_string = unit_string.strip() # Special case: dimensionless unit if unit_string == '1': return DIMENSIONLESS, FLOAT # Another special case: boolean variable if unit_string == 'boolean': return DIMENSIONLESS, BOOLEAN if unit_string == 'bool': raise TypeError("Use 'boolean' not 'bool' as the unit for a boolean " "variable.") # Yet another special case: integer variable if unit_string == 'integer': return DIMENSIONLESS, INTEGER # Check first whether the expression only refers to base units identifiers = get_identifiers(unit_string) for identifier in identifiers: if identifier not in _base_units: if identifier in DEFAULT_UNITS: # A known unit, but not a base unit base_unit = get_unit(DEFAULT_UNITS[identifier].dim) if not repr(base_unit) in _base_units: # Make sure that we don't suggest a unit that is not allowed # (should not happen, normally) base_unit = Unit(1, dim=base_unit.dim) raise ValueError( ('Unit specification refers to ' '"{identifier}", but this is not a base ' 'unit. Use "{base_unit}" ' 'instead.').format(identifier=identifier, base_unit=repr(base_unit))) else: # Not a known unit raise ValueError( ('Unit specification refers to ' '"{identifier}", but this is not a base ' 'unit. The following base units are ' 'allowed: ' '{allowed_units}.').format( identifier=identifier, allowed_units=_base_units_with_alternatives)) try: evaluated_unit = eval(unit_string, _base_units) except Exception as ex: raise ValueError(('Could not interpret "%s" as a unit specification: ' '%s') % (unit_string, ex)) # Check whether the result is a unit if not isinstance(evaluated_unit, Unit): if isinstance(evaluated_unit, Quantity): raise ValueError( ('"%s" does not evaluate to a unit but to a ' 'quantity -- make sure to only use units, e.g. ' '"siemens/metre**2" and not "1 * siemens/metre**2"') % unit_string) else: raise ValueError( ('"%s" does not evaluate to a unit, the result ' 'has type %s instead.' % (unit_string, type(evaluated_unit)))) # No error has been raised, all good return evaluated_unit.dim, FLOAT
def _init_1d(self): dimensions = self.dim unit = get_unit(dimensions) values = self.values dt = self.dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(t=second, result=unit) def timed_array_func(t): # We round according to the current defaultclock.dt K = _find_K(float(defaultclock.dt), dt) epsilon = dt / K i = np.clip(np.int_(np.round(np.asarray(t / epsilon)) / K), 0, len(values) - 1) return Quantity(values[i], dim=dimensions) Function.__init__(self, pyfunc=timed_array_func) # we use dynamic implementations because we want to do upsampling # in a way that avoids rounding problems with the group's dt def create_numpy_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) n_values = len(values) epsilon = dt / K def unitless_timed_array_func(t): timestep = np.clip(np.int_(np.round(t / epsilon) / K), 0, n_values - 1) return values[timestep] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit return unitless_timed_array_func self.implementations.add_dynamic_implementation( 'numpy', create_numpy_implementation) def create_cpp_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) support_code = ''' static inline double %NAME%(const double t) { const double epsilon = %DT% / %K%; int i = (int)((t/epsilon + 0.5)/%K%); if(i < 0) i = 0; if(i >= %NUM_VALUES%) i = %NUM_VALUES%-1; return _namespace%NAME%_values[i]; } '''.replace('%NAME%', self.name).replace('%DT%', '%.18f' % dt).replace( '%K%', str(K)).replace('%NUM_VALUES%', str(len(self.values))) cpp_code = {'support_code': support_code} return cpp_code def create_cpp_namespace(owner): return {'%s_values' % self.name: self.values} self.implementations.add_dynamic_implementation( 'cpp', code=create_cpp_implementation, namespace=create_cpp_namespace, name=self.name) def create_cython_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) code = ''' cdef double %NAME%(const double t): global _namespace%NAME%_values cdef double epsilon = %DT% / %K% cdef int i = (int)((t/epsilon + 0.5)/%K%) if i < 0: i = 0 if i >= %NUM_VALUES%: i = %NUM_VALUES% - 1 return _namespace%NAME%_values[i] '''.replace('%NAME%', self.name).replace('%DT%', '%.18f' % dt).replace( '%K%', str(K)).replace('%NUM_VALUES%', str(len(self.values))) return code def create_cython_namespace(owner): return {'%s_values' % self.name: self.values} self.implementations.add_dynamic_implementation( 'cython', code=create_cython_implementation, namespace=create_cython_namespace, name=self.name)
def _resolve_external(self, identifier, user_identifier=True, run_namespace=None, level=0): """ 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. 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``. 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. """ # 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 user_identifier: _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, 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 _init_2d(self): dimensions = self.dim unit = get_unit(dimensions) values = self.values dt = self.dt # Python implementation (with units), used when calling the TimedArray # directly, outside of a simulation @check_units(i=1, t=second, result=unit) def timed_array_func(t, i): # We round according to the current defaultclock.dt K = _find_K(float(defaultclock.dt), dt) epsilon = dt / K time_step = np.clip(np.int_(np.round(np.asarray(t / epsilon)) / K), 0, len(values) - 1) return Quantity(values[time_step, i], dim=dimensions) Function.__init__(self, pyfunc=timed_array_func) # we use dynamic implementations because we want to do upsampling # in a way that avoids rounding problems with the group's dt def create_numpy_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) n_values = len(values) epsilon = dt / K def unitless_timed_array_func(t, i): timestep = np.clip(np.int_(np.round(t / epsilon) / K), 0, n_values - 1) return values[timestep, i] unitless_timed_array_func._arg_units = [second] unitless_timed_array_func._return_unit = unit return unitless_timed_array_func self.implementations.add_dynamic_implementation( 'numpy', create_numpy_implementation) def create_cpp_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) support_code = ''' static inline double %NAME%(const double t, const int i) { const double epsilon = %DT% / %K%; if (i < 0 || i >= %COLS%) return NAN; int timestep = (int)((t/epsilon + 0.5)/%K%); if(timestep < 0) timestep = 0; else if(timestep >= %ROWS%) timestep = %ROWS%-1; return _namespace%NAME%_values[timestep*%COLS% + i]; } ''' support_code = replace( support_code, { '%NAME%': self.name, '%DT%': '%.18f' % dt, '%K%': str(K), '%COLS%': str(self.values.shape[1]), '%ROWS%': str(self.values.shape[0]) }) cpp_code = {'support_code': support_code} return cpp_code def create_cpp_namespace(owner): return { '%s_values' % self.name: self.values.astype(np.double, order='C', copy=False).ravel() } self.implementations.add_dynamic_implementation( 'cpp', code=create_cpp_implementation, namespace=create_cpp_namespace, name=self.name) def create_cython_implementation(owner): group_dt = owner.clock.dt_ K = _find_K(group_dt, dt) code = ''' cdef double %NAME%(const double t, const int i): global _namespace%NAME%_values cdef double epsilon = %DT% / %K% if i < 0 or i >= %COLS%: return _numpy.nan cdef int timestep = (int)((t/epsilon + 0.5)/%K%) if timestep < 0: timestep = 0 elif timestep >= %ROWS%: timestep = %ROWS%-1 return _namespace%NAME%_values[timestep*%COLS% + i] ''' code = replace( code, { '%NAME%': self.name, '%DT%': '%.18f' % dt, '%K%': str(K), '%COLS%': str(self.values.shape[1]), '%ROWS%': str(self.values.shape[0]) }) return code def create_cython_namespace(owner): return { '%s_values' % self.name: self.values.astype(np.double, order='C', copy=False).ravel() } self.implementations.add_dynamic_implementation( 'cython', code=create_cython_implementation, namespace=create_cython_namespace, name=self.name)