Ejemplo n.º 1
0
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))
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
    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)
Ejemplo n.º 8
0
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__))
Ejemplo n.º 9
0
    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")
Ejemplo n.º 10
0
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__))
Ejemplo n.º 11
0
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__))
Ejemplo n.º 12
0
    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)
Ejemplo n.º 13
0
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))