Coverage for nodimo / product.py: 100%
181 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-26 03:10 +0000
« 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
5"""
6Product
7=======
9This module contains the class to create the product of quantities.
11Classes
12-------
13Product
14 Creates the product of quantities.
15"""
17from sympy import srepr, Mul, S 1abcdef
18from sympy.printing.pretty.stringpict import prettyForm 1abcdef
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
26class Product(Quantity): 1abcdef
27 """Product of quantities.
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.
42 Attributes
43 ----------
44 factors : list[Quantity]
45 Multiplication factors.
47 Methods
48 -------
49 reduce()
50 Turns an unreduced quantity into a reduced quantity.
52 Raises
53 ------
54 TypeError
55 If at least one of the factors is not a quantity.
57 Examples
58 --------
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)
65 The ``*`` operator can also be used to create a product:
67 >>> d = a*b
68 """
70 _is_product = True 1abcdef
71 _is_derived = True 1abcdef
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
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
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
102 product_dimension = self._factors[0].dimension 1abcdef
103 for qty in self._factors[1:]: 1abcdef
104 product_dimension *= qty.dimension 1abcdef
106 dummy_name = 'Product' if name == '' else name 1abcdef
107 super().__init__( 1abcdef
108 name=dummy_name, **product_dimension, dependent=dependent, scaling=scaling
109 )
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
120 @property 1abcdef
121 def factors(self) -> list[Quantity]: 1abcdef
122 return self._factors 1abcdef
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
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
145 col._quantities = factors 1abcdef
146 col._clear_constants(only_ones=True) 1abcdef
148 return col._quantities 1abcdef
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
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
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
177 if self._is_constant: 1abcdef
178 self._name = _prettify_name(self._name, bold=True) 1abcdef
179 self._set_symbolic_quantity() 1abcdef
181 def _set_symbolic_product(self): 1abcdef
182 """Not currently used by the printing methods.
184 The symbolic attribute created here is not used for printing
185 because the factors don't follow input order.
186 """
188 qty_symb = [qty._symbolic for qty in self._factors] 1abcdef
189 self._symbolic = Mul(*qty_symb, evaluate=False) 1abcdef
191 def _copy(self): 1abcdef
192 qty_copy = eval(srepr(self)) 1abcdef
193 qty_copy._unreduced = self._unreduced 1abcdef
194 return qty_copy 1abcdef
196 def _key(self) -> tuple: 1abcdef
197 return (frozenset(self.reduce()._factors),) 1abcdef
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
213 self._numerator_quantities = numerator_quantities 1abcdef
214 self._denominator_quantities = denominator_quantities 1abcdef
215 self._is_quotient = bool(denominator_quantities) 1abcdef
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
225 return f'{class_name}({factors}{name}{dependent}{scaling}{reduce})' 1abcdef
227 def _sympystr(self, printer): 1abcdef
228 if self._name: 1abcdef
229 return printer._print(self._symbolic) 1abcdef
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
239 if not self._is_quotient: 1abcdef
240 return numerator 1abcdef
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
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
257 quotient = f'{numerator}/{denominator}' 1abcdef
259 return quotient 1abcdef
261 def _latex(self, printer): 1abcdef
262 if self._name: 1abcdef
263 return printer._print(self._symbolic) 1abcdef
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
273 if not self._is_quotient: 1abcdef
274 return numerator 1abcdef
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
284 quotient = f'\\frac{{{numerator}}}{{{denominator}}}' 1abcdef
286 return quotient 1abcdef
288 def _pretty(self, printer): 1abcdef
289 if self._name: 1abcdef
290 return printer._print(self._symbolic) 1abcdef
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
299 if not self._is_quotient: 1abcdef
300 return numerator 1abcdef
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
309 quotient = numerator / denominator 1abcdef
310 # Binding is changed from DIV to ATOM to avoid parenthesis.
311 quotient.binding = prettyForm.ATOM 1abcdef
313 return quotient 1abcdef