def check_dimensions(expr, unit_system="SI"): """Return expr if units in addends have the same base dimensions, else raise a ValueError.""" # the case of adding a number to a dimensional quantity # is ignored for the sake of SymPy core routines, so this # function will raise an error now if such an addend is # found. # Also, when doing substitutions, multiplicative constants # might be introduced, so remove those now from sympy.physics.units import UnitSystem unit_system = UnitSystem.get_unit_system(unit_system) def addDict(dict1, dict2): """Merge dictionaries by adding values of common keys and removing keys with value of 0.""" dict3 = {**dict1, **dict2} for key, value in dict3.items(): if key in dict1 and key in dict2: dict3[key] = value + dict1[key] return {key: val for key, val in dict3.items() if val != 0} adds = expr.atoms(Add) DIM_OF = unit_system.get_dimension_system().get_dimensional_dependencies for a in adds: deset = set() for ai in a.args: if ai.is_number: deset.add(()) continue dims = [] skip = False dimdict = {} for i in Mul.make_args(ai): if i.has(Quantity): i = Dimension(unit_system.get_dimensional_expr(i)) if i.has(Dimension): dimdict = addDict(dimdict, DIM_OF(i)) elif i.free_symbols: skip = True break dims.extend(dimdict.items()) if not skip: deset.add(tuple(sorted(dims, key=default_sort_key))) if len(deset) > 1: raise ValueError( "addends have incompatible dimensions: {}".format( deset)) # clear multiplicative constants on Dimensions which may be # left after substitution reps = {} for m in expr.atoms(Mul): if any(isinstance(i, Dimension) for i in m.args): reps[m] = m.func(*[i for i in m.args if not i.is_number]) return expr.xreplace(reps)
def test_get_units_non_prefixed(): from sympy.physics.units import volt, ohm unit_system = UnitSystem.get_unit_system("SI") units = unit_system.get_units_non_prefixed() for prefix in [ "giga", "tera", "peta", "exa", "zetta", "yotta", "kilo", "hecto", "deca", "deci", "centi", "milli", "micro", "nano", "pico", "femto", "atto", "zepto", "yocto" ]: for unit in units: assert isinstance( unit, Quantity), f"{unit} must be a Quantity, not {type(unit)}" assert not unit.is_prefixed, f"{unit} is marked as prefixed" assert not unit.is_physical_constant, f"{unit} is marked as physics constant" assert not unit.name.name.startswith( prefix), f"Unit {unit.name} has prefix {prefix}" assert volt in units assert ohm in units
def convert_to(expr, target_units, unit_system="SI"): """ Convert ``expr`` to the same expression with all of its units and quantities represented as factors of ``target_units``, whenever the dimension is compatible. ``target_units`` may be a single unit/quantity, or a collection of units/quantities. Examples ======== >>> from sympy.physics.units import speed_of_light, meter, gram, second, day >>> from sympy.physics.units import mile, newton, kilogram, atomic_mass_constant >>> from sympy.physics.units import kilometer, centimeter >>> from sympy.physics.units import gravitational_constant, hbar >>> from sympy.physics.units import convert_to >>> convert_to(mile, kilometer) 25146*kilometer/15625 >>> convert_to(mile, kilometer).n() 1.609344*kilometer >>> convert_to(speed_of_light, meter/second) 299792458*meter/second >>> convert_to(day, second) 86400*second >>> 3*newton 3*newton >>> convert_to(3*newton, kilogram*meter/second**2) 3*kilogram*meter/second**2 >>> convert_to(atomic_mass_constant, gram) 1.660539060e-24*gram Conversion to multiple units: >>> convert_to(speed_of_light, [meter, second]) 299792458*meter/second >>> convert_to(3*newton, [centimeter, gram, second]) 300000*centimeter*gram/second**2 Conversion to Planck units: >>> convert_to(atomic_mass_constant, [gravitational_constant, speed_of_light, hbar]).n() 7.62963087839509e-20*hbar**0.5*speed_of_light**0.5/gravitational_constant**0.5 """ from sympy.physics.units import UnitSystem unit_system = UnitSystem.get_unit_system(unit_system) if not isinstance(target_units, (Iterable, Tuple)): target_units = [target_units] if isinstance(expr, Add): return Add.fromiter( convert_to(i, target_units, unit_system) for i in expr.args) expr = sympify(expr) target_units = sympify(target_units) if not isinstance(expr, Quantity) and expr.has(Quantity): expr = expr.replace(lambda x: isinstance(x, Quantity), lambda x: x.convert_to(target_units, unit_system)) def get_total_scale_factor(expr): if isinstance(expr, Mul): return reduce(lambda x, y: x * y, [get_total_scale_factor(i) for i in expr.args]) elif isinstance(expr, Pow): return get_total_scale_factor(expr.base)**expr.exp elif isinstance(expr, Quantity): return unit_system.get_quantity_scale_factor(expr) return expr depmat = _get_conversion_matrix_for_expr(expr, target_units, unit_system) if depmat is None: return expr expr_scale_factor = get_total_scale_factor(expr) return expr_scale_factor * Mul.fromiter( (1 / get_total_scale_factor(u) * u)**p for u, p in zip(target_units, depmat))
def quantity_simplify(expr, across_dimensions: bool = False, unit_system=None): """Return an equivalent expression in which prefixes are replaced with numerical values and all units of a given dimension are the unified in a canonical manner by default. `across_dimensions` allows for units of different dimensions to be simplified together. `unit_system` must be specified if `across_dimensions` is True. Examples ======== >>> from sympy.physics.units.util import quantity_simplify >>> from sympy.physics.units.prefixes import kilo >>> from sympy.physics.units import foot, inch, joule, coulomb >>> quantity_simplify(kilo*foot*inch) 250*foot**2/3 >>> quantity_simplify(foot - 6*inch) foot/2 >>> quantity_simplify(5*joule/coulomb, across_dimensions=True, unit_system="SI") 5*volt """ if expr.is_Atom or not expr.has(Prefix, Quantity): return expr # replace all prefixes with numerical values p = expr.atoms(Prefix) expr = expr.xreplace({p: p.scale_factor for p in p}) # replace all quantities of given dimension with a canonical # quantity, chosen from those in the expression d = sift(expr.atoms(Quantity), lambda i: i.dimension) for k in d: if len(d[k]) == 1: continue v = list(ordered(d[k])) ref = v[0] / v[0].scale_factor expr = expr.xreplace({vi: ref * vi.scale_factor for vi in v[1:]}) if across_dimensions: # combine quantities of different dimensions into a single # quantity that is equivalent to the original expression if unit_system is None: raise ValueError( "unit_system must be specified if across_dimensions is True") unit_system = UnitSystem.get_unit_system(unit_system) dimension_system: DimensionSystem = unit_system.get_dimension_system() dim_expr = unit_system.get_dimensional_expr(expr) dim_deps = dimension_system.get_dimensional_dependencies( dim_expr, mark_dimensionless=True) target_dimension: Optional[Dimension] = None for ds_dim, ds_dim_deps in dimension_system.dimensional_dependencies.items( ): if ds_dim_deps == dim_deps: target_dimension = ds_dim break if target_dimension is None: # if we can't find a target dimension, we can't do anything. unsure how to handle this case. return expr target_unit = unit_system.derived_units.get(target_dimension) if target_unit: expr = convert_to(expr, target_unit, unit_system) return expr