Coverage for nodimo / quantity.py: 100%
180 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"""
6Quantity
7========
9This module contains the class to create a physical quantity.
11Classes
12-------
13Quantity
14 Creates a quantity.
15Constant
16 Creates a dimensionless constant.
17One
18 Creates a dimensionless number one.
19"""
21from sympy import sstr, srepr, latex, Symbol, Mul, Pow, Number 1abcdef
22from typing import Optional, Union 1abcdef
24from nodimo.dimension import Dimension 1abcdef
25from nodimo._internal import _sympify_number, _unsympify_number, _prettify_name 1abcdef
28class Quantity: 1abcdef
29 """Base class for quantities.
31 Most basic type of physical quantity, with attributes that are
32 useful in describing its dimensional properties.
34 Parameters
35 ----------
36 name : str
37 Name (symbol) to be used as the quantity representation.
38 dependent : bool, default=False
39 If ``True``, the quantity is considered dependent in a relation
40 of quantities
41 scaling : bool, default=False
42 If ``True``, the quantity can be used as scaling parameter.
43 **dimensions : Number
44 The dimensions of the quantity given as keyword arguments.
46 Attributes
47 ----------
48 name : str
49 Name (symbol) used as the quantity representation.
50 dimension : Dimension
51 The quantity dimension.
52 is_dependent : bool
53 If ``True``, the quantity is considered dependent in a relation
54 of quantities
55 is_scaling : bool
56 If ``True``, the quantity can be used as scaling parameter.
57 is_dimensionless : bool
58 If ``True``, the quantity is dimensionless.
60 Methods
61 -------
62 reduce()
63 Turns an unreduced quantity into a reduced quantity.
65 Raises
66 ------
67 TypeError
68 If the quantity name is not a string.
69 ValueError
70 If the quantity name is invalid.
71 ValueError
72 If the quantity is set as both dependent and scaling.
73 ValueError
74 If the quantity is set as scaling, but with no dimensions.
76 Examples
77 --------
78 Considering the dimensions mass ``M``, length ``L`` and time ``T``,
79 a force ``F`` can be defined as:
81 >>> from nodimo import Quantity
82 >>> F = Quantity('F', M=1, L=1, T=-2)
84 To define a dimensionless quantity ``A`` is sufficient to provide
85 just its name:
87 >>> A = Quantity('A')
89 To use a greek letter in symbolic expressions, just provide its
90 english representation as the name of the quantity:
92 >>> a = Quantity('alpha')
93 """
95 _is_power: bool = False 1abcdef
96 _is_product: bool = False 1abcdef
97 _is_derived: bool = False 1abcdef
98 _is_one: bool = False 1abcdef
100 def __init__( 1abcdef
101 self,
102 name: str,
103 dependent: bool = False,
104 scaling: bool = False,
105 **dimensions: Number,
106 ):
107 self._name: str 1abcdef
108 self._dimension: Dimension = Dimension(**dimensions) 1abcdef
109 self._is_dimensionless: bool = self._dimension._is_dimensionless 1abcdef
110 self._is_dependent: bool = bool(dependent) 1abcdef
111 self._is_scaling: bool = bool(scaling) 1abcdef
112 self._symbolic: Union[Symbol, Mul, Pow, Number] 1abcdef
113 self._is_constant: bool = False 1abcdef
114 self._is_number: bool = False 1abcdef
115 self._is_quotient: bool = False 1abcdef
116 self._is_reduced: bool = True 1abcdef
117 self._unreduced: Quantity = self 1abcdef
118 self._base: Quantity 1abcdef
119 self._exponent: Number 1abcdef
120 self._factors: list[Quantity] 1abcdef
121 self._validate_quantity() 1abcdef
122 self._set_quantity_name(name) 1abcdef
123 self._set_symbolic_quantity() 1abcdef
125 @property 1abcdef
126 def name(self) -> str: 1abcdef
127 return self._name 1abcdef
129 @property 1abcdef
130 def dimension(self) -> Dimension: 1abcdef
131 return self._dimension 1abcdef
133 @property 1abcdef
134 def is_dependent(self) -> bool: 1abcdef
135 return self._is_dependent 1abcdef
137 @property 1abcdef
138 def is_scaling(self) -> bool: 1abcdef
139 return self._is_scaling 1abcdef
141 @property 1abcdef
142 def is_dimensionless(self) -> bool: 1abcdef
143 return self._is_dimensionless 1abcdef
145 def _validate_quantity(self): 1abcdef
146 if self._is_dependent and self._is_constant: 1abcdef
147 raise ValueError("A constant can not be dependent") 1abcdef
148 elif self._is_scaling and self._is_constant: 1abcdef
149 raise ValueError("A constant can not be scaling") 1abcdef
150 elif self._is_dependent and self._is_scaling: 1abcdef
151 raise ValueError("A quantity can not be both dependent and scaling") 1abcdef
152 elif self._is_scaling and self._is_dimensionless: 1abcdef
153 raise ValueError("A quantity can not be both scaling and dimensionless") 1abcdef
155 def _set_quantity_name(self, name: str): 1abcdef
156 if not isinstance(name, str): 1abcdef
157 raise TypeError("Quantity name must be a string") 1abcdef
158 elif name.strip() == '': 1abcdef
159 raise ValueError("Invalid quantity name") 1abcdef
161 self._name = name 1abcdef
163 def _set_symbolic_quantity(self): 1abcdef
164 self._symbolic = Symbol(self._name) 1abcdef
166 def _copy(self): 1abcdef
167 qty_copy = eval(srepr(self)) 1abcdef
168 qty_copy._unreduced = self._unreduced 1abcdef
169 return qty_copy 1abcdef
171 def reduce(self): 1abcdef
172 if self._is_reduced: 1abcdef
173 return self 1abcdef
174 elif self._is_power: 1abcdef
175 from nodimo.power import Power 1abcdef
177 base = self.base.reduce() 1abcdef
178 exponent = self.exponent 1abcdef
179 reduced_power = Power( 1abcdef
180 base,
181 exponent,
182 name=self._name,
183 dependent=self._is_dependent,
184 scaling=self._is_scaling,
185 )
186 reduced_power._unreduced = self 1abcdef
187 return reduced_power 1abcdef
188 elif self._is_product: 1abcdef
189 from nodimo.product import Product 1abcdef
191 factors = [qty.reduce() for qty in self.factors] 1abcdef
192 reduced_product = Product( 1abcdef
193 *factors,
194 name=self._name,
195 dependent=self._is_dependent,
196 scaling=self._is_scaling,
197 )
198 reduced_product._unreduced = self 1abcdef
199 return reduced_product 1abcdef
201 def _key(self) -> tuple: 1abcdef
202 return (self._name, frozenset(self._dimension.items())) 1abcdef
204 def __hash__(self) -> int: 1abcdef
205 return hash(self._key()) 1abcdef
207 def __eq__(self, other) -> bool: 1abcdef
208 if self is other: 1abcdef
209 return True 1abcdef
210 elif isinstance(other, type(self)): 1abcdef
211 return self._key() == other._key() 1abcdef
212 return False 1abcdef
214 def __mul__(self, other): 1abcdef
215 if not isinstance(other, Quantity): 1abcdef
216 raise NotImplementedError(f"{self} and {other} cannot be multiplied") 1abcdef
218 from nodimo.product import Product 1abcdef
220 return Product(self, other) 1abcdef
222 def __pow__(self, exponent: Number): 1abcdef
223 from nodimo.power import Power 1abcdef
225 return Power(self, exponent) 1abcdef
227 def __truediv__(self, other): 1abcdef
228 if not isinstance(other, Quantity): 1abcdef
229 raise NotImplementedError(f"{other} cannot divide {self}") 1abcdef
231 return self * other**-1 1abcdef
233 def __str__(self) -> str: 1abcdef
234 return sstr(self) 1abcdef
236 def _repr_latex_(self): 1abcdef
237 """Latex representation according to IPython/Jupyter."""
239 return f'$\\displaystyle {latex(self)}$' 1abcdef
241 def _sympy_(self): 1abcdef
242 return self._symbolic 1abcdef
244 def _sympyrepr(self, printer): 1abcdef
245 """Developer string representation according to Sympy."""
247 class_name = type(self).__name__ 1abcdef
248 name = f"'{self.name}'" if self.name is not None else '' 1abcdef
249 dimensions = '' 1abcdef
250 if not self._is_dimensionless: 1abcdef
251 dim_exp = [] 1abcdef
252 for dim, exp in self._dimension.items(): 1abcdef
253 exp_ = _unsympify_number(exp) 1abcdef
254 dim_exp.append(f'{dim}={repr(exp_)}') 1abcdef
255 dimensions = f", {', '.join(dim_exp)}" 1abcdef
256 dependent = f', dependent=True' if self._is_dependent else '' 1abcdef
257 scaling = f', scaling=True' if self._is_scaling else '' 1abcdef
259 return f'{class_name}({name}{dimensions}{dependent}{scaling})' 1abcdef
261 def _sympystr(self, printer): 1abcdef
262 """User string representation according to Sympy."""
264 if self._symbolic.is_number: 1abcdef
265 printer._settings['root_notation'] = True 1abcdef
266 else:
267 printer._settings['root_notation'] = False 1abcdef
269 return printer._print(self._symbolic) 1abcdef
271 _pretty = _latex = _sympystr 1abcdef
272 __repr__ = __str__ 1abcdef
275# Alias for the class Quantity to facilitate the instantiation.
276Q = Quantity 1abcdef
279class Constant(Quantity): 1abcdef
280 """Dimensionless constant.
282 Constants are most commonly just dimensionless numbers. Here, they
283 can be represented by letters, which are be provided as the value
284 parameter. If the value is a string of characters, the constant is
285 printed in bold on the screen to avoid confusion with instances of
286 Quantity.
288 Constants were implemented to allow the creation of expressions that
289 contain dimensionless numbers. In general, these numbers are only
290 useful in the mathematical equations that represent the model. In
291 Nodimo, constants are used only for aesthetic purposes, mainly in
292 product of quantities.
294 Examples
295 --------
296 * Drag coefficient
298 Dimensions: mass ``M``, length ``L`` and time ``T``.
300 Quantities: drag force ``Fd``, fluid density ``rho``,
301 velocity ``V``, reference area ``A``, ``half`` and
302 drag coefficient ``Cd``.
304 In this example, the ``reduce`` parameter set to ``False`` is
305 important to keep the constant in the denominator of the expression.
307 >>> from nodimo import Quantity, Power, Product
308 >>> from nodimo.quantity import Constant
309 >>> Fd = Quantity('F_D', M=1, L=1, T=-2)
310 >>> rho = Quantity('rho', M=1, L=-3)
311 >>> V = Quantity('V', L=1, T=-1)
312 >>> A = Quantity('A', L=2)
313 >>> half = Constant(1/2)
314 >>> half_inv = Power(half, -1, reduce=False)
315 >>> Cd = Product(Fd, half_inv, rho**-1, V**-2, A**-1, reduce=False)
316 """
318 def __new__(cls, value: Union[str, Number]): 1abcdef
319 converted_value = cls._convert_value(value) 1abcdef
320 if converted_value == 1: 1abcdef
321 return One() 1abcdef
322 return super().__new__(cls) 1abcdef
324 def __init__(self, value: Union[str, Number]): 1abcdef
325 self._constant_name: str 1abcdef
326 converted_value = self._convert_value(value) 1abcdef
327 super().__init__(str(converted_value)) 1abcdef
328 self._set_constant(converted_value) 1abcdef
330 @classmethod 1abcdef
331 def _convert_value(self, value: Union[str, Number]) -> Union[str, Number]: 1abcdef
332 converted_value: Union[str, Number]
333 try: 1abcdef
334 converted_value = _sympify_number(value) 1abcdef
335 except: 1abcdef
336 if not isinstance(value, str): 1abcdef
337 raise TypeError(f"Constant value must be a string or a number") 1abcdef
338 converted_value = value 1abcdef
340 return converted_value 1abcdef
342 def _set_constant(self, value: Union[str, Number]): 1abcdef
343 self._is_constant = True 1abcdef
344 self._constant_name = str(value) 1abcdef
345 if isinstance(value, str): 1abcdef
346 self._name = _prettify_name(value, bold=True) 1abcdef
347 self._set_symbolic_quantity() 1abcdef
348 else:
349 self._symbolic = value 1abcdef
350 self._is_number = True 1abcdef
351 if value.is_Rational and not value.is_Integer: 1abcdef
352 self._is_quotient = True 1abcdef
353 elif value.is_Mul: 1abcdef
354 for num in value.args: 1abcdef
355 if num.is_Rational and not num.is_Integer: 1abcdef
356 self._is_quotient = True 1abcdef
357 break 1abcdef
358 elif num.is_Pow and Pow(num, 1).exp < 0: 1abcdef
359 self._is_quotient = True 1abcdef
360 break 1abcdef
362 def _sympyrepr(self, printer): 1abcdef
363 class_name = type(self).__name__ 1abcdef
364 if self._is_number and self._symbolic.is_Number: 1abcdef
365 value = _unsympify_number(self._symbolic) 1abcdef
366 else:
367 value = f"'{self._constant_name}'" 1abcdef
369 return f'{class_name}({value})' 1abcdef
372class One(Constant): 1abcdef
373 """Dimensionless one."""
375 _is_one = True 1abcdef
377 def __new__(cls, *args, **kwargs): 1abcdef
378 return super(Constant, cls).__new__(cls) 1abcdef
380 def __init__(self, *args, **kwargs): 1abcdef
381 super().__init__(1) 1abcdef
383 def _sympyrepr(self, printer): 1abcdef
384 return f'{type(self).__name__}()' 1abcdef