def __repr__(self): s = '<' + self.type + ' ' + self.varname if not self.expr is None: s += ': ' + self.expr.code s += ' (Unit: ' + get_unit_for_display(self.dim) if len(self.flags): s += ', flags: ' + ', '.join(self.flags) s += ')>' return s
def __str__(self): if self.type == DIFFERENTIAL_EQUATION: s = 'd' + self.varname + '/dt' else: s = self.varname if not self.expr is None: s += ' = ' + str(self.expr) s += ' : ' + get_unit_for_display(self.dim) if len(self.flags): s += ' (' + ', '.join(self.flags) + ')' return s
def 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_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))