Coverage for nodimo / product.py: 100%

181 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-26 03:10 +0000

1# ┓• Licensed under the MIT License 

2# ┏┓┏┓┏┫┓┏┳┓┏┓ Copyright (c) 2024 Rodrigo Castro 

3# ┛┗┗┛┗┻┗┛┗┗┗┛ https://nodimo.readthedocs.io 

4 

5""" 

6Product 

7======= 

8 

9This module contains the class to create the product of quantities. 

10 

11Classes 

12------- 

13Product 

14 Creates the product of quantities. 

15""" 

16 

17from sympy import srepr, Mul, S 1abcdef

18from sympy.printing.pretty.stringpict import prettyForm 1abcdef

19 

20from nodimo.quantity import Quantity, Constant, One 1abcdef

21from nodimo.collection import Collection 1abcdef

22from nodimo.power import Power 1abcdef

23from nodimo._internal import _prettify_name 1abcdef

24 

25 

26class Product(Quantity): 1abcdef

27 """Product of quantities. 

28 

29 Parameters 

30 ---------- 

31 *quantities : Quantity 

32 Quantities to be multiplied. 

33 name : str, default='' 

34 Name (symbol) to be used as the product representation. 

35 dependent : bool, default=False 

36 If ``True``, the product is dependent. 

37 scaling : bool, default=False 

38 If ``True``, the product can be used as scaling parameter. 

39 reduce : bool, default=True 

40 If ``True``, the product is simplified or rearranged. 

41 

42 Attributes 

43 ---------- 

44 factors : list[Quantity] 

45 Multiplication factors. 

46 

47 Methods 

48 ------- 

49 reduce() 

50 Turns an unreduced quantity into a reduced quantity. 

51 

52 Raises 

53 ------ 

54 TypeError 

55 If at least one of the factors is not a quantity. 

56 

57 Examples 

58 -------- 

59 

60 >>> from nodimo import Quantity, Product 

61 >>> a = Quantity('a', A=-2, B=1) 

62 >>> b = Quantity('b', A=2, B=-1) 

63 >>> c = Product(a, b) 

64 

65 The ``*`` operator can also be used to create a product: 

66  

67 >>> d = a*b 

68 """ 

69 

70 _is_product = True 1abcdef

71 _is_derived = True 1abcdef

72 

73 def __new__( 1abcdef

74 cls, 

75 *factors: Quantity, 

76 name: str = '', 

77 dependent: bool = False, 

78 scaling: bool = False, 

79 reduce: bool = True, 

80 ): 

81 if not all(isinstance(qty, Quantity) for qty in factors): 1abcdef

82 raise TypeError(f"All product inputs must be quantities") 1abcdef

83 elif reduce: 1abcdef

84 factors = cls._simplify_factors(*factors) 1abcdef

85 

86 if len(factors) == 0: 1abcdef

87 return One() 1abcdef

88 elif len(factors) == 1: 1abcdef

89 return factors[0] 1abcdef

90 return super().__new__(cls) 1abcdef

91 

92 def __init__( 1abcdef

93 self, 

94 *factors: Quantity, 

95 name: str = '', 

96 dependent: bool = False, 

97 scaling: bool = False, 

98 reduce: bool = True, 

99 ): 

100 self._preset_product(*factors, reduce=reduce) 1abcdef

101 

102 product_dimension = self._factors[0].dimension 1abcdef

103 for qty in self._factors[1:]: 1abcdef

104 product_dimension *= qty.dimension 1abcdef

105 

106 dummy_name = 'Product' if name == '' else name 1abcdef

107 super().__init__( 1abcdef

108 name=dummy_name, **product_dimension, dependent=dependent, scaling=scaling 

109 ) 

110 

111 self._set_product(reduce=reduce) 1abcdef

112 self._validate_quantity() 1abcdef

113 self._numerator_quantities: list[Quantity] 1abcdef

114 self._denominator_quantities: list[Quantity] 1abcdef

115 if name == '': 1abcdef

116 self._name = name 1abcdef

117 self._set_numerator_quantities() 1abcdef

118 self._set_symbolic_product() 1abcdef

119 

120 @property 1abcdef

121 def factors(self) -> list[Quantity]: 1abcdef

122 return self._factors 1abcdef

123 

124 @classmethod 1abcdef

125 def _simplify_factors(cls, *initial_factors: Quantity): 1abcdef

126 col = Collection(*initial_factors) 1abcdef

127 col._quantities = col._disassembled_quantities 1abcdef

128 col._set_constants() 1abcdef

129 factors: list[Quantity] = [Constant(Mul(*col._number_constants))] 1abcdef

130 col._clear_constants(only_numbers=True) 1abcdef

131 

132 if len(col._quantities) > len(col._base_quantities): 1abcdef

133 for base_qty in col._base_quantities: 1abcdef

134 exponent = S.Zero 1abcdef

135 for qty in col._quantities: 1abcdef

136 if qty == base_qty: 1abcdef

137 exponent += S.One 1abcdef

138 elif qty._is_power: 1abcdef

139 if qty._base == base_qty: 1abcdef

140 exponent += qty._exponent 1abcdef

141 factors.append(Power(base_qty, exponent)) 1abcdef

142 else: 

143 factors.extend(col._quantities) 1abcdef

144 

145 col._quantities = factors 1abcdef

146 col._clear_constants(only_ones=True) 1abcdef

147 

148 return col._quantities 1abcdef

149 

150 def _preset_product(self, *factors: Quantity, reduce: bool = True): 1abcdef

151 if reduce: 1abcdef

152 self._factors = self._simplify_factors(*factors) 1abcdef

153 else: 

154 self._factors = list(factors) 1abcdef

155 

156 def _set_product(self, reduce: bool = True): 1abcdef

157 if bool(reduce): 1abcdef

158 self._is_reduced = True 1abcdef

159 elif any(not qty._is_reduced for qty in self._factors): 1abcdef

160 self._is_reduced = False 1abcdef

161 else: 

162 reduced_factors = self._simplify_factors(*self._factors) 1abcdef

163 if frozenset(self._factors) == frozenset(reduced_factors): 1abcdef

164 self._is_reduced = True 1abcdef

165 else: 

166 self._is_reduced = False 1abcdef

167 

168 if all(qty._is_constant for qty in self._factors): 1abcdef

169 self._is_constant = True 1abcdef

170 self._is_number = all(qty._is_number for qty in self._factors) 1abcdef

171 else: 

172 if 'reduced_quantities' not in locals(): 1abcdef

173 reduced_factors = self._simplify_factors(*self._factors) 1abcdef

174 self._is_constant = all(qty._is_constant for qty in reduced_factors) 1abcdef

175 self._is_number = all(qty._is_number for qty in reduced_factors) 1abcdef

176 

177 if self._is_constant: 1abcdef

178 self._name = _prettify_name(self._name, bold=True) 1abcdef

179 self._set_symbolic_quantity() 1abcdef

180 

181 def _set_symbolic_product(self): 1abcdef

182 """Not currently used by the printing methods. 

183 

184 The symbolic attribute created here is not used for printing 

185 because the factors don't follow input order. 

186 """ 

187 

188 qty_symb = [qty._symbolic for qty in self._factors] 1abcdef

189 self._symbolic = Mul(*qty_symb, evaluate=False) 1abcdef

190 

191 def _copy(self): 1abcdef

192 qty_copy = eval(srepr(self)) 1abcdef

193 qty_copy._unreduced = self._unreduced 1abcdef

194 return qty_copy 1abcdef

195 

196 def _key(self) -> tuple: 1abcdef

197 return (frozenset(self.reduce()._factors),) 1abcdef

198 

199 def _set_numerator_quantities(self): 1abcdef

200 numerator_quantities = [] 1abcdef

201 denominator_quantities = [] 1abcdef

202 for qty in self._factors: 1abcdef

203 if qty._is_power and qty.exponent < 0 and not qty._name: 1abcdef

204 qty._exponent *= -1 1abcdef

205 qtyinv = qty._copy() 1abcdef

206 qty._exponent *= -1 1abcdef

207 denominator_quantities.append(qtyinv) 1abcdef

208 else: 

209 numerator_quantities.append(qty) 1abcdef

210 if len(numerator_quantities) == 0: 1abcdef

211 numerator_quantities.append(One()) 1abcdef

212 

213 self._numerator_quantities = numerator_quantities 1abcdef

214 self._denominator_quantities = denominator_quantities 1abcdef

215 self._is_quotient = bool(denominator_quantities) 1abcdef

216 

217 def _sympyrepr(self, printer): 1abcdef

218 class_name = type(self).__name__ 1abcdef

219 factors = ', '.join(printer._print(qty) for qty in self._factors) 1abcdef

220 name = f", name='{self._name}'" if self._name else '' 1abcdef

221 dependent = f', dependent=True' if self._is_dependent else '' 1abcdef

222 scaling = f', scaling=True' if self._is_scaling else '' 1abcdef

223 reduce = f', reduce=False' if not self._is_reduced else '' 1abcdef

224 

225 return f'{class_name}({factors}{name}{dependent}{scaling}{reduce})' 1abcdef

226 

227 def _sympystr(self, printer): 1abcdef

228 if self._name: 1abcdef

229 return printer._print(self._symbolic) 1abcdef

230 

231 numerator_quantities = [] 1abcdef

232 for i, qty in enumerate(self._numerator_quantities): 1abcdef

233 quantity = printer._print(qty) 1abcdef

234 if qty._is_quotient or (i > 0 and qty._is_number and qty._symbolic < 0): 1abcdef

235 quantity = f'({quantity})' 1abcdef

236 numerator_quantities.append(quantity) 1abcdef

237 numerator = '*'.join(numerator_quantities) 1abcdef

238 

239 if not self._is_quotient: 1abcdef

240 return numerator 1abcdef

241 

242 denominator_quantities = [] 1abcdef

243 for i, qty in enumerate(self._denominator_quantities): 1abcdef

244 quantity = printer._print(qty) 1abcdef

245 if qty._is_quotient or (i > 0 and qty._is_number and qty._symbolic < 0): 1abcdef

246 quantity = f'({quantity})' 1abcdef

247 denominator_quantities.append(quantity) 1abcdef

248 denominator = '*'.join(denominator_quantities) 1abcdef

249 

250 if len(self._denominator_quantities) > 1: 1abcdef

251 denominator = f'({denominator})' 1abcdef

252 elif len(self._denominator_quantities) == 1: 1abcdef

253 qty = self._denominator_quantities[0] 1abcdef

254 if qty._is_product and not qty._is_quotient: 1abcdef

255 denominator = f'({denominator})' 1abcdef

256 

257 quotient = f'{numerator}/{denominator}' 1abcdef

258 

259 return quotient 1abcdef

260 

261 def _latex(self, printer): 1abcdef

262 if self._name: 1abcdef

263 return printer._print(self._symbolic) 1abcdef

264 

265 numerator_quantities = [] 1abcdef

266 for i, qty in enumerate(self._numerator_quantities): 1abcdef

267 quantity = printer._print(qty) 1abcdef

268 if i > 0 and (qty._is_number and qty._symbolic < 0): 1abcdef

269 quantity = f'\\left({quantity}\\right)' 1abcdef

270 numerator_quantities.append(quantity) 1abcdef

271 numerator = ' '.join(numerator_quantities) 1abcdef

272 

273 if not self._is_quotient: 1abcdef

274 return numerator 1abcdef

275 

276 denominator_quantities = [] 1abcdef

277 for i, qty in enumerate(self._denominator_quantities): 1abcdef

278 quantity = printer._print(qty) 1abcdef

279 if i > 0 and (qty._is_number and qty._symbolic < 0): 1abcdef

280 quantity = f'\\left({quantity}\\right)' 1abcdef

281 denominator_quantities.append(quantity) 1abcdef

282 denominator = ' '.join(denominator_quantities) 1abcdef

283 

284 quotient = f'\\frac{{{numerator}}}{{{denominator}}}' 1abcdef

285 

286 return quotient 1abcdef

287 

288 def _pretty(self, printer): 1abcdef

289 if self._name: 1abcdef

290 return printer._print(self._symbolic) 1abcdef

291 

292 numerator = printer._print(self._numerator_quantities[0]) 1abcdef

293 for qty in self._numerator_quantities[1:]: 1abcdef

294 quantity = printer._print(qty) 1abcdef

295 if qty._is_number and qty._symbolic < 0: 1abcdef

296 quantity = prettyForm(*quantity.parens()) 1abcdef

297 numerator *= quantity 1abcdef

298 

299 if not self._is_quotient: 1abcdef

300 return numerator 1abcdef

301 

302 denominator = printer._print(self._denominator_quantities[0]) 1abcdef

303 for qty in self._denominator_quantities[1:]: 1abcdef

304 quantity = printer._print(qty) 1abcdef

305 if qty._is_number and qty._symbolic < 0: 1abcdef

306 quantity = prettyForm(*quantity.parens()) 1abcdef

307 denominator *= quantity 1abcdef

308 

309 quotient = numerator / denominator 1abcdef

310 # Binding is changed from DIV to ATOM to avoid parenthesis. 

311 quotient.binding = prettyForm.ATOM 1abcdef

312 

313 return quotient 1abcdef