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

1""" 

2Several methods to simplify expressions involving unit objects. 

3""" 

4from functools import reduce 

5from collections.abc import Iterable 

6from typing import Optional 

7 

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 

21 

22 

23def _get_conversion_matrix_for_expr(expr, target_units, unit_system): 

24 from sympy.matrices.dense import Matrix 

25 

26 dimension_system = unit_system.get_dimension_system() 

27 

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) 

33 

34 if not canon_expr_units.issubset(set(canon_dim_units)): 

35 return None 

36 

37 seen = set() 

38 canon_dim_units = [i for i in canon_dim_units if not (i in seen or seen.add(i))] 

39 

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]) 

42 

43 try: 

44 res_exponents = camat.solve(exprmat) 

45 except NonInvertibleMatrixError: 

46 return None 

47 

48 return res_exponents 

49 

50 

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. 

55 

56 ``target_units`` may be a single unit/quantity, or a collection of 

57 units/quantities. 

58 

59 Examples 

60 ======== 

61 

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 

81 

82 Conversion to multiple units: 

83 

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 

88 

89 Conversion to Planck units: 

90 

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 

93 

94 """ 

95 from sympy.physics.units import UnitSystem 

96 unit_system = UnitSystem.get_unit_system(unit_system) 

97 

98 if not isinstance(target_units, (Iterable, Tuple)): 

99 target_units = [target_units] 

100 

101 if isinstance(expr, Add): 

102 return Add.fromiter(convert_to(i, target_units, unit_system) 

103 for i in expr.args) 

104 

105 expr = sympify(expr) 

106 target_units = sympify(target_units) 

107 

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)) 

111 

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 

121 

122 depmat = _get_conversion_matrix_for_expr(expr, target_units, unit_system) 

123 if depmat is None: 

124 return expr 

125 

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)) 

130 

131 

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. 

137 

138 `unit_system` must be specified if `across_dimensions` is True. 

139 

140 Examples 

141 ======== 

142 

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 """ 

153 

154 if expr.is_Atom or not expr.has(Prefix, Quantity): 

155 return expr 

156 

157 # replace all prefixes with numerical values 

158 p = expr.atoms(Prefix) 

159 expr = expr.xreplace({p: p.scale_factor for p in p}) 

160 

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:]}) 

170 

171 if across_dimensions: 

172 # combine quantities of different dimensions into a single 

173 # quantity that is equivalent to the original expression 

174 

175 if unit_system is None: 

176 raise ValueError("unit_system must be specified if across_dimensions is True") 

177 

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) 

182 

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 

188 

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 

192 

193 target_unit = unit_system.derived_units.get(target_dimension) 

194 if target_unit: 

195 expr = convert_to(expr, target_unit, unit_system) 

196 

197 return expr 

198 

199 

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 

209 

210 from sympy.physics.units import UnitSystem 

211 unit_system = UnitSystem.get_unit_system(unit_system) 

212 

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} 

221 

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)) 

247 

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]) 

255 

256 return expr.xreplace(reps)