Coverage for /usr/lib/python3/dist-packages/sympy/physics/units/util.py: 16%
127 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2Several methods to simplify expressions involving unit objects.
3"""
4from functools import reduce
5from collections.abc import Iterable
6from typing import Optional
8from sympy import default_sort_key
9from sympy.core.add import Add
10from sympy.core.containers import Tuple
11from sympy.core.mul import Mul
12from sympy.core.power import Pow
13from sympy.core.sorting import ordered
14from sympy.core.sympify import sympify
15from sympy.matrices.common import NonInvertibleMatrixError
16from sympy.physics.units.dimensions import Dimension, DimensionSystem
17from sympy.physics.units.prefixes import Prefix
18from sympy.physics.units.quantities import Quantity
19from sympy.physics.units.unitsystem import UnitSystem
20from sympy.utilities.iterables import sift
23def _get_conversion_matrix_for_expr(expr, target_units, unit_system):
24 from sympy.matrices.dense import Matrix
26 dimension_system = unit_system.get_dimension_system()
28 expr_dim = Dimension(unit_system.get_dimensional_expr(expr))
29 dim_dependencies = dimension_system.get_dimensional_dependencies(expr_dim, mark_dimensionless=True)
30 target_dims = [Dimension(unit_system.get_dimensional_expr(x)) for x in target_units]
31 canon_dim_units = [i for x in target_dims for i in dimension_system.get_dimensional_dependencies(x, mark_dimensionless=True)]
32 canon_expr_units = set(dim_dependencies)
34 if not canon_expr_units.issubset(set(canon_dim_units)):
35 return None
37 seen = set()
38 canon_dim_units = [i for i in canon_dim_units if not (i in seen or seen.add(i))]
40 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])
41 exprmat = Matrix([dim_dependencies.get(k, 0) for k in canon_dim_units])
43 try:
44 res_exponents = camat.solve(exprmat)
45 except NonInvertibleMatrixError:
46 return None
48 return res_exponents
51def convert_to(expr, target_units, unit_system="SI"):
52 """
53 Convert ``expr`` to the same expression with all of its units and quantities
54 represented as factors of ``target_units``, whenever the dimension is compatible.
56 ``target_units`` may be a single unit/quantity, or a collection of
57 units/quantities.
59 Examples
60 ========
62 >>> from sympy.physics.units import speed_of_light, meter, gram, second, day
63 >>> from sympy.physics.units import mile, newton, kilogram, atomic_mass_constant
64 >>> from sympy.physics.units import kilometer, centimeter
65 >>> from sympy.physics.units import gravitational_constant, hbar
66 >>> from sympy.physics.units import convert_to
67 >>> convert_to(mile, kilometer)
68 25146*kilometer/15625
69 >>> convert_to(mile, kilometer).n()
70 1.609344*kilometer
71 >>> convert_to(speed_of_light, meter/second)
72 299792458*meter/second
73 >>> convert_to(day, second)
74 86400*second
75 >>> 3*newton
76 3*newton
77 >>> convert_to(3*newton, kilogram*meter/second**2)
78 3*kilogram*meter/second**2
79 >>> convert_to(atomic_mass_constant, gram)
80 1.660539060e-24*gram
82 Conversion to multiple units:
84 >>> convert_to(speed_of_light, [meter, second])
85 299792458*meter/second
86 >>> convert_to(3*newton, [centimeter, gram, second])
87 300000*centimeter*gram/second**2
89 Conversion to Planck units:
91 >>> convert_to(atomic_mass_constant, [gravitational_constant, speed_of_light, hbar]).n()
92 7.62963087839509e-20*hbar**0.5*speed_of_light**0.5/gravitational_constant**0.5
94 """
95 from sympy.physics.units import UnitSystem
96 unit_system = UnitSystem.get_unit_system(unit_system)
98 if not isinstance(target_units, (Iterable, Tuple)):
99 target_units = [target_units]
101 if isinstance(expr, Add):
102 return Add.fromiter(convert_to(i, target_units, unit_system)
103 for i in expr.args)
105 expr = sympify(expr)
106 target_units = sympify(target_units)
108 if not isinstance(expr, Quantity) and expr.has(Quantity):
109 expr = expr.replace(lambda x: isinstance(x, Quantity),
110 lambda x: x.convert_to(target_units, unit_system))
112 def get_total_scale_factor(expr):
113 if isinstance(expr, Mul):
114 return reduce(lambda x, y: x * y,
115 [get_total_scale_factor(i) for i in expr.args])
116 elif isinstance(expr, Pow):
117 return get_total_scale_factor(expr.base) ** expr.exp
118 elif isinstance(expr, Quantity):
119 return unit_system.get_quantity_scale_factor(expr)
120 return expr
122 depmat = _get_conversion_matrix_for_expr(expr, target_units, unit_system)
123 if depmat is None:
124 return expr
126 expr_scale_factor = get_total_scale_factor(expr)
127 return expr_scale_factor * Mul.fromiter(
128 (1/get_total_scale_factor(u)*u)**p for u, p in
129 zip(target_units, depmat))
132def quantity_simplify(expr, across_dimensions: bool=False, unit_system=None):
133 """Return an equivalent expression in which prefixes are replaced
134 with numerical values and all units of a given dimension are the
135 unified in a canonical manner by default. `across_dimensions` allows
136 for units of different dimensions to be simplified together.
138 `unit_system` must be specified if `across_dimensions` is True.
140 Examples
141 ========
143 >>> from sympy.physics.units.util import quantity_simplify
144 >>> from sympy.physics.units.prefixes import kilo
145 >>> from sympy.physics.units import foot, inch, joule, coulomb
146 >>> quantity_simplify(kilo*foot*inch)
147 250*foot**2/3
148 >>> quantity_simplify(foot - 6*inch)
149 foot/2
150 >>> quantity_simplify(5*joule/coulomb, across_dimensions=True, unit_system="SI")
151 5*volt
152 """
154 if expr.is_Atom or not expr.has(Prefix, Quantity):
155 return expr
157 # replace all prefixes with numerical values
158 p = expr.atoms(Prefix)
159 expr = expr.xreplace({p: p.scale_factor for p in p})
161 # replace all quantities of given dimension with a canonical
162 # quantity, chosen from those in the expression
163 d = sift(expr.atoms(Quantity), lambda i: i.dimension)
164 for k in d:
165 if len(d[k]) == 1:
166 continue
167 v = list(ordered(d[k]))
168 ref = v[0]/v[0].scale_factor
169 expr = expr.xreplace({vi: ref*vi.scale_factor for vi in v[1:]})
171 if across_dimensions:
172 # combine quantities of different dimensions into a single
173 # quantity that is equivalent to the original expression
175 if unit_system is None:
176 raise ValueError("unit_system must be specified if across_dimensions is True")
178 unit_system = UnitSystem.get_unit_system(unit_system)
179 dimension_system: DimensionSystem = unit_system.get_dimension_system()
180 dim_expr = unit_system.get_dimensional_expr(expr)
181 dim_deps = dimension_system.get_dimensional_dependencies(dim_expr, mark_dimensionless=True)
183 target_dimension: Optional[Dimension] = None
184 for ds_dim, ds_dim_deps in dimension_system.dimensional_dependencies.items():
185 if ds_dim_deps == dim_deps:
186 target_dimension = ds_dim
187 break
189 if target_dimension is None:
190 # if we can't find a target dimension, we can't do anything. unsure how to handle this case.
191 return expr
193 target_unit = unit_system.derived_units.get(target_dimension)
194 if target_unit:
195 expr = convert_to(expr, target_unit, unit_system)
197 return expr
200def check_dimensions(expr, unit_system="SI"):
201 """Return expr if units in addends have the same
202 base dimensions, else raise a ValueError."""
203 # the case of adding a number to a dimensional quantity
204 # is ignored for the sake of SymPy core routines, so this
205 # function will raise an error now if such an addend is
206 # found.
207 # Also, when doing substitutions, multiplicative constants
208 # might be introduced, so remove those now
210 from sympy.physics.units import UnitSystem
211 unit_system = UnitSystem.get_unit_system(unit_system)
213 def addDict(dict1, dict2):
214 """Merge dictionaries by adding values of common keys and
215 removing keys with value of 0."""
216 dict3 = {**dict1, **dict2}
217 for key, value in dict3.items():
218 if key in dict1 and key in dict2:
219 dict3[key] = value + dict1[key]
220 return {key:val for key, val in dict3.items() if val != 0}
222 adds = expr.atoms(Add)
223 DIM_OF = unit_system.get_dimension_system().get_dimensional_dependencies
224 for a in adds:
225 deset = set()
226 for ai in a.args:
227 if ai.is_number:
228 deset.add(())
229 continue
230 dims = []
231 skip = False
232 dimdict = {}
233 for i in Mul.make_args(ai):
234 if i.has(Quantity):
235 i = Dimension(unit_system.get_dimensional_expr(i))
236 if i.has(Dimension):
237 dimdict = addDict(dimdict, DIM_OF(i))
238 elif i.free_symbols:
239 skip = True
240 break
241 dims.extend(dimdict.items())
242 if not skip:
243 deset.add(tuple(sorted(dims, key=default_sort_key)))
244 if len(deset) > 1:
245 raise ValueError(
246 "addends have incompatible dimensions: {}".format(deset))
248 # clear multiplicative constants on Dimensions which may be
249 # left after substitution
250 reps = {}
251 for m in expr.atoms(Mul):
252 if any(isinstance(i, Dimension) for i in m.args):
253 reps[m] = m.func(*[
254 i for i in m.args if not i.is_number])
256 return expr.xreplace(reps)