def test_str_repr(): ''' Test that str representations do not raise any errors and that repr fullfills eval(repr(x)) == x. ''' from numpy import array # necessary for evaluating repr units_which_should_exist = [ metre, meter, kilogram, kilogramme, second, amp, kelvin, mole, candle, radian, steradian, hertz, newton, pascal, joule, watt, coulomb, volt, farad, ohm, siemens, weber, tesla, henry, lumen, lux, becquerel, gray, sievert, katal, gram, gramme, molar, liter, litre ] # scaled versions of all these units should exist (we just check farad as an example) some_scaled_units = [ Yfarad, Zfarad, Efarad, Pfarad, Tfarad, Gfarad, Mfarad, kfarad, hfarad, dafarad, dfarad, cfarad, mfarad, ufarad, nfarad, pfarad, ffarad, afarad, zfarad, yfarad ] # some powered units powered_units = [cmetre2, Yfarad3] # Combined units complex_units = [ (kgram * metre2) / (amp * second3), 5 * (kgram * metre2) / (amp * second3), metre * second**-1, 10 * metre * second**-1, array([1, 2, 3]) * kmetre / second, np.ones(3) * nS / cm**2, Unit(1, dim=get_or_create_dimension(length=5, time=2)), 8000 * umetre**3, [0.0001, 10000] * umetre**3, 1 / metre, 1 / (coulomb * metre**2), Unit(1) / second, 3. * mM, 5 * mole / liter, 7 * liter / meter3 ] unitless = [second / second, 5 * second / second, Unit(1)] for u in itertools.chain(units_which_should_exist, some_scaled_units, powered_units, complex_units, unitless): assert (len(str(u)) > 0) assert_allclose(eval(repr(u)), u) # test the `DIMENSIONLESS` object assert str(DIMENSIONLESS) == '1' assert repr(DIMENSIONLESS) == 'Dimension()' # test DimensionMismatchError (only that it works without raising an error for error in [ DimensionMismatchError('A description'), DimensionMismatchError('A description', DIMENSIONLESS), DimensionMismatchError('A description', DIMENSIONLESS, second.dim) ]: assert len(str(error)) assert len(repr(error))
def check_units(self, group, run_namespace): ''' Check all the units for consistency. Parameters ---------- group : `Group` The group providing the context run_namespace : dict-like, optional An additional namespace that is used for variable lookup (if not defined, the implicit namespace of local variables is used). level : int, optional How much further to go up in the stack to find the calling frame Raises ------ DimensionMismatchError In case of any inconsistencies. ''' all_variables = dict(group.variables) external = frozenset().union( *[expr.identifiers for _, expr in self.eq_expressions]) external -= set(all_variables.keys()) resolved_namespace = group.resolve_all( external, run_namespace, user_identifiers=external) # all variables are user defined all_variables.update(resolved_namespace) for var, eq in self._equations.iteritems(): if eq.type == PARAMETER: # no need to check units for parameters continue if eq.type == DIFFERENTIAL_EQUATION: try: check_dimensions(str(eq.expr), self.dimensions[var] / second.dim, all_variables) except DimensionMismatchError as ex: raise DimensionMismatchError( ('Inconsistent units in ' 'differential equation ' 'defining variable %s:' '\n%s') % (eq.varname, ex.desc), *ex.dims) elif eq.type == SUBEXPRESSION: try: check_dimensions(str(eq.expr), self.dimensions[var], all_variables) except DimensionMismatchError as ex: raise DimensionMismatchError( ('Inconsistent units in ' 'subexpression %s:' '\n%s') % (eq.varname, ex.desc), *ex.dims) else: raise AssertionError('Unknown equation type: "%s"' % eq.type)
def spatialneuron_segment(neuron, item): ''' Selects a segment from `SpatialNeuron` neuron, where item is a slice of either compartment indexes or distances. Note a: segment is not a `SpatialNeuron`, only a `Group`. ''' if isinstance(item, slice) and isinstance(item.start, Quantity): if item.step is not None: raise ValueError('Cannot specify a step size for slicing based' 'on length.') start, stop = item.start, item.stop if (not have_same_dimensions(start, meter) or not have_same_dimensions(stop, meter)): raise DimensionMismatchError('Start and stop should have units ' 'of meter', start, stop) # Convert to integers (compartment numbers) indices = neuron.morphology.indices[item] start, stop = indices[0], indices[-1] + 1 elif not isinstance(item, slice) and hasattr(item, 'indices'): start, stop = to_start_stop(item.indices[:], neuron._N) else: start, stop = to_start_stop(item, neuron._N) if start >= stop: raise IndexError('Illegal start/end values for subgroup, %d>=%d' % (start, stop)) return Subgroup(neuron, start, stop)
def spatialneuron_segment(neuron, item): ''' Selects a segment from `SpatialNeuron` neuron, where item is a slice of either compartment indexes or distances. Note a: segment is not a `SpatialNeuron`, only a `Group`. ''' if not isinstance(item, slice): raise TypeError( 'Subgroups can only be constructed using slicing syntax') start, stop, step = item.start, item.stop, item.step if step is None: step = 1 if step != 1: raise IndexError('Subgroups have to be contiguous') if isinstance(start, Quantity): if not have_same_dimensions( start, meter) or not have_same_dimensions(stop, meter): raise DimensionMismatchError( 'Start and stop should have units of meter', start, stop) # Convert to integers (compartment numbers) indices = neuron.morphology.indices[item] start, stop = indices[0], indices[-1] + 1 if start >= stop: raise IndexError('Illegal start/end values for subgroup, %d>=%d' % (start, stop)) return Subgroup(neuron, start, stop)
def spatialneuron_segment(neuron, item): """ Selects a segment from `SpatialNeuron` neuron, where item is a slice of either compartment indexes or distances. Note a: segment is not a `SpatialNeuron`, only a `Group`. """ if isinstance(item, slice) and isinstance(item.start, Quantity): if item.step is not None: raise ValueError("Cannot specify a step size for slicing based" "on length.") start, stop = item.start, item.stop if (not have_same_dimensions(start, meter) or not have_same_dimensions(stop, meter)): raise DimensionMismatchError("Start and stop should have units " "of meter", start, stop) # Convert to integers (compartment numbers) indices = neuron.morphology.indices[item] start, stop = indices[0], indices[-1] + 1 elif not isinstance(item, slice) and hasattr(item, 'indices'): start, stop = to_start_stop(item.indices[:], neuron._N) else: start, stop = to_start_stop(item, neuron._N) if isinstance(neuron, SpatialSubgroup): start += neuron.start stop += neuron.start if start >= stop: raise IndexError(f'Illegal start/end values for subgroup, {int(start)}>={int(stop)}') if isinstance(neuron, SpatialSubgroup): # Note that the start/stop values calculated above are always # absolute values, even for subgroups neuron = neuron.source return Subgroup(neuron, start, stop)
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)) self._weight = weight self._target_var = target_var if isinstance(weight, str): weight = '(%s)' % weight else: weight_dims = get_dimensions(weight) target_dims = target.variables[target_var].dim # 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_dims, target_dims): raise DimensionMismatchError( ('The provided weight does not ' 'have the same unit as the ' 'target variable "%s"') % target_var, weight_dims, target_dims) weight = repr(weight) self._N = N self._rate = rate 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 __setattr__(self, key, value): # attribute access is switched off until this attribute is created by # _enable_group_attributes if not hasattr( self, '_group_attribute_access_active') or key in self.__dict__: object.__setattr__(self, key, value) elif key in self._linked_variables: if not isinstance(value, LinkedVariable): raise ValueError( ('Cannot set a linked variable directly, link ' 'it to another variable using "linked_var".')) linked_var = value.variable if isinstance(linked_var, DynamicArrayVariable): raise NotImplementedError(('Linking to variable %s is not ' 'supported, can only link to ' 'state variables of fixed ' 'size.') % linked_var.name) eq = self.equations[key] if eq.dim is not linked_var.dim: raise DimensionMismatchError( ('Unit of variable %s does not ' 'match its link target %s') % (key, linked_var.name)) if not isinstance(linked_var, Subexpression): var_length = len(linked_var) else: var_length = len(linked_var.owner) if value.index is not None: try: index_array = np.asarray(value.index) if not np.issubsctype(index_array.dtype, np.int): raise TypeError() except TypeError: raise TypeError(('The index for a linked variable has ' 'to be an integer array')) size = len(index_array) source_index = value.group.variables.indices[value.name] if source_index not in ('_idx', '0'): # we are indexing into an already indexed variable, # calculate the indexing into the target variable index_array = value.group.variables[ source_index].get_value()[index_array] if not index_array.ndim == 1 or size != len(self): raise TypeError( ('Index array for linked variable %s ' 'has to be a one-dimensional array of ' 'length %d, but has shape ' '%s') % (key, len(self), str(index_array.shape))) if min(index_array) < 0 or max(index_array) >= var_length: raise ValueError('Index array for linked variable %s ' 'contains values outside of the valid ' 'range [0, %d[' % (key, var_length)) self.variables.add_array('_%s_indices' % key, size=size, dtype=index_array.dtype, constant=True, read_only=True, values=index_array) index = '_%s_indices' % key else: if linked_var.scalar or (var_length == 1 and self._N != 1): index = '0' else: index = value.group.variables.indices[value.name] if index == '_idx': target_length = var_length else: target_length = len(value.group.variables[index]) # we need a name for the index that does not clash with # other names and a reference to the index new_index = '_' + value.name + '_index_' + index self.variables.add_reference(new_index, value.group, index) index = new_index if len(self) != target_length: raise ValueError( ('Cannot link variable %s to %s, the size of ' 'the target group does not match ' '(%d != %d). You can provide an indexing ' 'scheme with the "index" keyword to link ' 'groups with different sizes') % (key, linked_var.name, len(self), target_length)) self.variables.add_reference(key, value.group, value.name, index=index) log_msg = ('Setting {target}.{targetvar} as a link to ' '{source}.{sourcevar}').format( target=self.name, targetvar=key, source=value.variable.owner.name, sourcevar=value.variable.name) if index is not None: log_msg += '(using "{index}" as index variable)'.format( index=index) logger.diagnostic(log_msg) else: if isinstance(value, LinkedVariable): raise TypeError( ('Cannot link variable %s, it has to be marked ' 'as a linked variable with "(linked)" in the ' 'model equations.') % key) else: Group.__setattr__(self, key, value, level=1)
def parse_expression_unit(expr, namespace, variables): ''' Returns the unit value of an expression, and checks its validity Parameters ---------- expr : str The expression to check. namespace : dict-like The namespace of external variables. variables : dict of `Variable` objects The information about the internal 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. Notes ----- Currently, functions do not work, see comments in function. ''' # 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 ast.Name: name = expr.id if name in variables: return variables[name].unit elif name in namespace: return get_unit_fast(namespace[name]) elif name in ['True', 'False']: return Unit(1) else: raise ValueError('Unknown identifier %s' % name) elif expr.__class__ is ast.Num: return get_unit_fast(1) elif expr.__class__ is ast.BoolOp: # check that the units are valid in each subexpression for node in expr.values: parse_expression_unit(node, namespace, variables) # but the result is a bool, so we just return 1 as the unit return get_unit_fast(1) 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_unit(node, namespace, variables)) for left, right in zip(subunits[:-1], subunits[1:]): if not have_same_dimensions(left, right): raise DimensionMismatchError( "Comparison of expressions with different units", *[getattr(u, 'dim', 1) for u in subunits]) # but the result is a bool, so we just return 1 as the unit return get_unit_fast(1) elif expr.__class__ is ast.Call: if len(expr.keywords): raise ValueError("Keyword arguments not supported.") elif expr.starargs is not None: raise ValueError("Variable number of arguments not supported") elif expr.kwargs is not None: raise ValueError("Keyword arguments not supported") arg_units = [ parse_expression_unit(arg, namespace, variables) for arg in expr.args ] func = namespace.get(expr.func.id, variables.get(expr.func, 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) for idx, arg_unit in enumerate(arg_units): # A "None" in func._arg_units means: No matter what unit if (func._arg_units[idx] is not None and not have_same_dimensions(arg_unit, func._arg_units[idx])): raise DimensionMismatchError( ('Argument number %d for function ' '%s does not have the correct ' 'units' % (idx + 1, expr.func.id)), arg_unit, func._arg_units[idx]) if isinstance(func._return_unit, (Unit, int)): # Function always returns the same unit return get_unit_fast(func._return_unit) else: # Function returns a unit that depends on the arguments return func._return_unit(*arg_units) elif expr.__class__ is ast.BinOp: op = expr.op.__class__.__name__ left = parse_expression_unit(expr.left, namespace, variables) right = parse_expression_unit(expr.right, namespace, variables) if op == 'Add' or op == 'Sub': u = left + right elif op == 'Mult': u = left * right elif op == 'Div': u = left / right elif op == 'Pow': if have_same_dimensions(left, 1) and have_same_dimensions( right, 1): return get_unit_fast(1) n = _get_value_from_expression(expr.right, namespace, variables) u = left**n elif op == 'Mod': u = left % right 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_unit(expr.operand, namespace, variables) if op == 'Not': return get_unit_fast(1) else: return u else: raise SyntaxError('Unsupported operation ' + str(expr.__class__))
def before_run(self, run_namespace=None, level=0): ''' before_run(namespace) Prepares the `Network` for a run. Objects in the `Network` are sorted into the correct running order, and their `BrianObject.before_run` methods are called. Parameters ---------- namespace : dict-like, optional A namespace in which objects which do not define their own namespace will be run. ''' from brian2.devices.device import get_device, all_devices prefs.check_all_validated() # Check names in the network for uniqueness names = [obj.name for obj in self.objects] non_unique_names = [ name for name, count in Counter(names).iteritems() if count > 1 ] if len(non_unique_names): formatted_names = ', '.join("'%s'" % name for name in non_unique_names) raise ValueError( 'All objects in a network need to have unique ' 'names, the following name(s) were used more than ' 'once: %s' % formatted_names) self._stopped = False Network._globally_stopped = False device = get_device() if device.network_schedule is not None: # The device defines a fixed network schedule if device.network_schedule != self.schedule: # TODO: The human-readable name of a device should be easier to get device_name = all_devices.keys()[all_devices.values().index( device)] logger.warn( ("The selected device '{device_name}' only " "supports a fixed schedule, but this schedule is " "not consistent with the network's schedule. The " "simulation will use the device's schedule.\n" "Device schedule: {device.network_schedule}\n" "Network schedule: {net.schedule}\n" "Set the network schedule explicitly or set the " "core.network.default_schedule preference to " "avoid this warning.").format(device_name=device_name, device=device, net=self), name_suffix='schedule_conflict', once=True) self._sort_objects() logger.debug( "Preparing network {self.name} with {numobj} " "objects: {objnames}".format( self=self, numobj=len(self.objects), objnames=', '.join(obj.name for obj in self.objects)), "before_run") self.check_dependencies() for obj in self.objects: if obj.active: try: obj.before_run(run_namespace, level=level + 2) except DimensionMismatchError as ex: raise DimensionMismatchError( ('An error occured preparing ' 'object "%s":\n%s') % (obj.name, ex.desc), *ex.dims) # Check that no object has been run as part of another network before for obj in self.objects: if obj._network is None: obj._network = self.id elif obj._network != self.id: raise RuntimeError(('%s has already been run in the ' 'context of another network. Use ' 'add/remove to change the objects ' 'in a simulated network instead of ' 'creating a new one.') % obj.name) logger.debug( "Network {self.name} has {num} " "clocks: {clocknames}".format( self=self, num=len(self._clocks), clocknames=', '.join(obj.name for obj in self._clocks)), "before_run")
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 or expr.__class__ is getattr(ast, 'Constant', None)): # Python 3.8 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 = get_unit_for_display(left_dim) right_unit = get_unit_for_display(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 parse_expression_unit(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 Unit(1) 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].unit elif name in ['True', 'False']: return Unit(1) else: raise KeyError('Unknown identifier %s' % name) elif expr.__class__ is ast.Num: return get_unit_fast(1) elif expr.__class__ is ast.BoolOp: # check that the units are valid in each subexpression for node in expr.values: parse_expression_unit(node, variables) # but the result is a bool, so we just return 1 as the unit return get_unit_fast(1) 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_unit(node, variables)) for left, right in zip(subunits[:-1], subunits[1:]): if not have_same_dimensions(left, right): raise DimensionMismatchError( "Comparison of expressions with different units", *[getattr(u, 'dim', 1) for u in subunits]) # but the result is a bool, so we just return 1 as the unit return get_unit_fast(1) 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") arg_units = [ parse_expression_unit(arg, variables) for arg in expr.args ] 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(arg_units): raise SyntaxError( 'Function %s was called with %d parameters, ' 'needs %d.' % (expr.func.id, len(arg_units), len(func._arg_units))) for idx, arg_unit in enumerate(arg_units): # A "None" in func._arg_units means: No matter what unit if (func._arg_units[idx] is not None and not have_same_dimensions(arg_unit, func._arg_units[idx])): raise DimensionMismatchError( ('Argument number %d for function ' '%s does not have the correct ' 'units' % (idx + 1, expr.func.id)), arg_unit, func._arg_units[idx]) if isinstance(func._return_unit, (Unit, int)): # Function always returns the same unit return get_unit_fast(func._return_unit) else: # Function returns a unit that depends on the arguments return func._return_unit(*arg_units) elif expr.__class__ is ast.BinOp: op = expr.op.__class__.__name__ left = parse_expression_unit(expr.left, variables) right = parse_expression_unit(expr.right, variables) if op == 'Add' or op == 'Sub': u = left + right elif op == 'Mult': u = left * right elif op == 'Div': u = left / right elif op == 'Pow': if have_same_dimensions(left, 1) and have_same_dimensions( right, 1): return get_unit_fast(1) n = _get_value_from_expression(expr.right, variables) u = left**n elif op == 'Mod': u = left % right 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_unit(expr.operand, variables) if op == 'Not': return get_unit_fast(1) else: return u else: raise SyntaxError('Unsupported operation ' + str(expr.__class__))
def __setattr__(self, key, value): # attribute access is switched off until this attribute is created by # _enable_group_attributes if not hasattr( self, '_group_attribute_access_active') or key in self.__dict__: object.__setattr__(self, key, value) elif key in self._linked_variables: if not isinstance(value, LinkedVariable): raise ValueError("Cannot set a linked variable directly, link " "it to another variable using 'linked_var'.") linked_var = value.variable if isinstance(linked_var, DynamicArrayVariable): raise NotImplementedError( f"Linking to variable {linked_var.name} is " f"not supported, can only link to " f"state variables of fixed size.") eq = self.equations[key] if eq.dim is not linked_var.dim: raise DimensionMismatchError( f"Unit of variable '{key}' does not " f"match its link target " f"'{linked_var.name}'") if not isinstance(linked_var, Subexpression): var_length = len(linked_var) else: var_length = len(linked_var.owner) if value.index is not None: try: index_array = np.asarray(value.index) if not np.issubsctype(index_array.dtype, int): raise TypeError() except TypeError: raise TypeError("The index for a linked variable has " "to be an integer array") size = len(index_array) source_index = value.group.variables.indices[value.name] if source_index not in ('_idx', '0'): # we are indexing into an already indexed variable, # calculate the indexing into the target variable index_array = value.group.variables[ source_index].get_value()[index_array] if not index_array.ndim == 1 or size != len(self): raise TypeError(f"Index array for linked variable '{key}' " f"has to be a one-dimensional array of " f"length {len(self)}, but has shape " f"{index_array.shape!s}") if min(index_array) < 0 or max(index_array) >= var_length: raise ValueError(f"Index array for linked variable {key} " f"contains values outside of the valid " f"range [0, {var_length}[") self.variables.add_array(f'_{key}_indices', size=size, dtype=index_array.dtype, constant=True, read_only=True, values=index_array) index = f'_{key}_indices' else: if linked_var.scalar or (var_length == 1 and self._N != 1): index = '0' else: index = value.group.variables.indices[value.name] if index == '_idx': target_length = var_length else: target_length = len(value.group.variables[index]) # we need a name for the index that does not clash with # other names and a reference to the index new_index = f"_{value.name}_index_{index}" self.variables.add_reference(new_index, value.group, index) index = new_index if len(self) != target_length: raise ValueError( f"Cannot link variable '{key}' to " f"'{linked_var.name}', the size of the " f"target group does not match " f"({len(self)} != {target_length}). You can " f"provide an indexing scheme with the " f"'index' keyword to link groups with " f"different sizes") self.variables.add_reference(key, value.group, value.name, index=index) source = value.variable.owner.name, sourcevar = value.variable.name log_msg = (f"Setting {self.name}.{key} as a link to " f"{source}.{sourcevar}") if index is not None: log_msg += f'(using "{index}" as index variable)' logger.diagnostic(log_msg) else: if isinstance(value, LinkedVariable): raise TypeError( f"Cannot link variable '{key}', it has to be marked " f"as a linked variable with '(linked)' in the model " f"equations.") else: Group.__setattr__(self, key, value, level=1)
def parse_expression_dimensions(expr, variables, orig_expr=None): """ 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, str): orig_expr = expr 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(f'Do not know how to handle value {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( f'{name} was used like a variable/constant, but it is a function.', ("<string>", expr.lineno, expr.col_offset + 1, orig_expr)) if name in variables: return get_dimensions(variables[name]) elif name in ['True', 'False']: return DIMENSIONLESS else: raise KeyError(f'Unknown identifier {name}') elif (expr.__class__ is ast.Num or expr.__class__ is getattr(ast, 'Constant', None)): # Python 3.8 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, orig_expr=orig_expr) # 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, orig_expr=orig_expr)) for left_dim, right_dim in zip(subunits[:-1], subunits[1:]): if not have_same_dimensions(left_dim, right_dim): left_expr = NodeRenderer().render_node(expr.left) right_expr = NodeRenderer().render_node(expr.comparators[0]) dim_left = get_dimensions(left_dim) dim_right = get_dimensions(right_dim) msg = ( f"Comparison of expressions with different units. Expression " f"'{left_expr}' has unit ({dim_left}), while expression " f"'{right_expr}' has units ({dim_right}).") 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( f'Unknown function {expr.func.id}', ("<string>", expr.lineno, expr.col_offset + 1, orig_expr)) if not hasattr(func, '_arg_units') or not hasattr( func, '_return_unit'): raise ValueError( f"Function {expr.func_id} does not specify how it " f"deals with units.") if len(func._arg_units) != len(expr.args): raise SyntaxError( f"Function '{expr.func.id}' was called with " f"{len(expr.args)} parameters, needs " f"{len(func._arg_units)}.", ("<string>", expr.lineno, expr.col_offset + len(expr.func.id) + 1, orig_expr)) for idx, (arg, expected_unit) in enumerate(zip(expr.args, func._arg_units)): arg_unit = parse_expression_dimensions(arg, variables, orig_expr=orig_expr) # A "None" in func._arg_units means: No matter what unit if expected_unit is None: continue # A string means: same unit as other argument elif isinstance(expected_unit, str): arg_idx = func._arg_names.index(expected_unit) expected_unit = parse_expression_dimensions( expr.args[arg_idx], variables, orig_expr=orig_expr) if not have_same_dimensions(arg_unit, expected_unit): msg = ( f'Argument number {idx + 1} for function ' f'{expr.func.id} was supposed to have the ' f'same units as argument number {arg_idx + 1}, but ' f"'{NodeRenderer().render_node(arg)}' has unit " f'{get_unit_for_display(arg_unit)}, while ' f"'{NodeRenderer().render_node(expr.args[arg_idx])}' " f'has unit {get_unit_for_display(expected_unit)}') raise DimensionMismatchError(msg) elif expected_unit == bool: if not is_boolean_expression(arg, variables): rendered_arg = NodeRenderer().render_node(arg) raise TypeError( f"Argument number {idx + 1} for function " f"'{expr.func.id}' was expected to be a boolean " f"value, but is '{rendered_arg}'.") else: if not have_same_dimensions(arg_unit, expected_unit): rendered_arg = NodeRenderer().render_node(arg) arg_unit_dim = get_dimensions(arg_unit) expected_unit_dim = get_dimensions(expected_unit) msg = ( f"Argument number {idx+1} for function {expr.func.id} does " f"not have the correct units. Expression '{rendered_arg}' " f"has units ({arg_unit_dim}), but " f"should be " f"({expected_unit_dim}).") 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, orig_expr=orig_expr) 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, orig_expr=orig_expr) right_dim = parse_expression_dimensions(expr.right, variables, orig_expr=orig_expr) 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 = get_unit_for_display(left_dim) right_unit = get_unit_for_display(right_dim) error_msg = ( f"Expression '{left_str} {op_symbol} {right_str}' uses " f"inconsistent units ('{left_str}' has unit " f"{left_unit}; '{right_str}' " f"has 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): if left_dim is DIMENSIONLESS: col_offset = expr.right.col_offset + 1 else: col_offset = expr.left.col_offset + 1 raise SyntaxError( "Floor division can only be used on " "dimensionless values.", ("<string>", expr.lineno, col_offset, orig_expr)) 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( f"Unsupported operation {op}", ("<string>", expr.lineno, getattr(expr.left, 'end_col_offset', len(NodeRenderer().render_node(expr.left))) + 1, orig_expr)) 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, orig_expr=orig_expr) if op == 'Not': return DIMENSIONLESS else: return u else: raise SyntaxError( f"Unsupported operation {str(expr.__class__.__name__)}", ("<string>", expr.lineno, expr.col_offset + 1, orig_expr))