def unitsAreConsistent(expr, targetUnits=None): """Check if an expression's units are compatible. If `targetUnits` provided, also checks for compatibility for converting to `targetUnits`. Raises UnitMisMatchError when applicable. Notes: If `targetUnits` is a units.Dimension, requires `expr` to evaluate to units with same units.Dimension (eg. must include units in `expr`) If `expr` has dimension of `1` (unit-less), returns `True` barring other errors or targetUnits being units.Dimension. :param expr: sympy.Expr :param targetUnits: str\\units.Quantity\\units.Dimension :return: True or raises UnitMisMatchError """ logger.log(logging.DEBUG - 1, f'unitsAreConsistent({expr}, {targetUnits})') try: # rough check within expression check_dimensions(expr) logger.log(logging.DEBUG - 1, f"check_dimensions -> {bool(check_dimensions(expr))}") except ValueError as e: raise UnitMisMatchError(repr(e)) except TypeError as e: raise UnitMisMatchError(repr(e)) expr_dim = getDimension(expr) logger.log(logging.DEBUG - 1, f"expr_dim = {expr_dim}") if expr_dim is None: raise UnitMisMatchError("Dimension could not be determined") if targetUnits is None: return True # get dimensions of target units if isinstance(targetUnits, str): try: targetUnits = unitSubs[targetUnits] except KeyError: targetUnits = parseUnits(targetUnits) target_dim = getDimension(targetUnits) logger.log(logging.DEBUG - 1, f"target_dim = {target_dim}") if isinstance(targetUnits, units.Dimension): if targetUnits.name == expr_dim.name: return True else: raise UnitMisMatchError( f"{expr_dim} is not specified {target_dim}") # compare dimensions if expr_dim.name == 1 or expr_dim.name == target_dim.name: return True else: raise UnitMisMatchError(f"{expr_dim} incompatible with {target_dim}")
def test_check_dimensions(): x = symbols('x') assert check_dimensions(inch + x) == inch + x assert check_dimensions(length + x) == length + x # after subs we get 2*length; check will clear the constant assert check_dimensions((length + x).subs(x, length)) == length assert check_dimensions(newton * meter + joule) == joule + meter * newton raises(ValueError, lambda: check_dimensions(inch + 1)) raises(ValueError, lambda: check_dimensions(length + 1)) raises(ValueError, lambda: check_dimensions(length + time)) raises(ValueError, lambda: check_dimensions(meter + second)) raises(ValueError, lambda: check_dimensions(2 * meter + second)) raises(ValueError, lambda: check_dimensions(2 * meter + 3 * second)) raises(ValueError, lambda: check_dimensions(1 / second + 1 / meter)) raises(ValueError, lambda: check_dimensions(2 * meter * (mile + centimeter) + km))
def test_check_dimensions(): x = symbols('x') assert check_dimensions(inch + x) == inch + x assert check_dimensions(length + x) == length + x # after subs we get 2*length; check will clear the constant assert check_dimensions((length + x).subs(x, length)) == length raises(ValueError, lambda: check_dimensions(inch + 1)) raises(ValueError, lambda: check_dimensions(length + 1)) raises(ValueError, lambda: check_dimensions(length + time)) raises(ValueError, lambda: check_dimensions(meter + second)) raises(ValueError, lambda: check_dimensions(2 * meter + second)) raises(ValueError, lambda: check_dimensions(2 * meter + 3 * second)) raises(ValueError, lambda: check_dimensions(1 / second + 1 / meter)) raises(ValueError, lambda: check_dimensions(2 * meter*(mile + centimeter) + km))