257 lines
9.4 KiB
Python
257 lines
9.4 KiB
Python
"""
|
|
Several methods to simplify expressions involving unit objects.
|
|
"""
|
|
from functools import reduce
|
|
from collections.abc import Iterable
|
|
from typing import Optional
|
|
|
|
from sympy import default_sort_key
|
|
from sympy.core.add import Add
|
|
from sympy.core.containers import Tuple
|
|
from sympy.core.mul import Mul
|
|
from sympy.core.power import Pow
|
|
from sympy.core.sorting import ordered
|
|
from sympy.core.sympify import sympify
|
|
from sympy.matrices.common import NonInvertibleMatrixError
|
|
from sympy.physics.units.dimensions import Dimension, DimensionSystem
|
|
from sympy.physics.units.prefixes import Prefix
|
|
from sympy.physics.units.quantities import Quantity
|
|
from sympy.physics.units.unitsystem import UnitSystem
|
|
from sympy.utilities.iterables import sift
|
|
|
|
|
|
def _get_conversion_matrix_for_expr(expr, target_units, unit_system):
|
|
from sympy.matrices.dense import Matrix
|
|
|
|
dimension_system = unit_system.get_dimension_system()
|
|
|
|
expr_dim = Dimension(unit_system.get_dimensional_expr(expr))
|
|
dim_dependencies = dimension_system.get_dimensional_dependencies(expr_dim, mark_dimensionless=True)
|
|
target_dims = [Dimension(unit_system.get_dimensional_expr(x)) for x in target_units]
|
|
canon_dim_units = [i for x in target_dims for i in dimension_system.get_dimensional_dependencies(x, mark_dimensionless=True)]
|
|
canon_expr_units = set(dim_dependencies)
|
|
|
|
if not canon_expr_units.issubset(set(canon_dim_units)):
|
|
return None
|
|
|
|
seen = set()
|
|
canon_dim_units = [i for i in canon_dim_units if not (i in seen or seen.add(i))]
|
|
|
|
camat = Matrix([[dimension_system.get_dimensional_dependencies(i, mark_dimensionless=True).get(j, 0) for i in target_dims] for j in canon_dim_units])
|
|
exprmat = Matrix([dim_dependencies.get(k, 0) for k in canon_dim_units])
|
|
|
|
try:
|
|
res_exponents = camat.solve(exprmat)
|
|
except NonInvertibleMatrixError:
|
|
return None
|
|
|
|
return res_exponents
|
|
|
|
|
|
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
|
|
|
|
|
|
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)
|