Coverage for nodimo / groups.py: 100%
206 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"""
6Groups
7======
9This module contains classes to create groups of quantities, which are
10specific types of collections.
12Classes
13-------
14Group
15 Creates a group of quantities.
16HomogeneorusGroup
17 Creates a homogeneous group of quantities.
18ScalingGroup
19 Creates a Group of scaling quantities.
20IndependentGroup
21 Creates a independent group of quantities.
22DimensionalGroup
23 Creates a dimensional group of derived quantities.
24"""
26from sympy import Number, Matrix, zeros, eye, S 1abcdef
27from sympy.printing.pretty.stringpict import prettyForm 1abcdef
28from typing import Union 1abcdef
30from nodimo.quantity import Quantity 1abcdef
31from nodimo.collection import Collection 1abcdef
32from nodimo.power import Power 1abcdef
33from nodimo.product import Product 1abcdef
34from nodimo._internal import _unsympify_number, _show_nodimo_warning 1abcdef
37class Group(Collection): 1abcdef
38 """Group of quantities.
40 Equivalent to a Collection, but without duplicate quantities.
42 Warns
43 -----
44 NodimoWarning
45 Removed duplicate quantities.
46 """
48 def __init__(self, *quantities: Quantity): 1abcdef
49 super().__init__(*quantities) 1abcdef
50 self._set_group() 1abcdef
52 def _set_group(self): 1abcdef
53 self._clear_duplicate_quantities() 1abcdef
55 def _clear_duplicate_quantities(self): 1abcdef
56 duplicate_quantities = [] 1abcdef
57 clear_quantities = [] 1abcdef
58 for qty in self._quantities: 1abcdef
59 if qty not in clear_quantities: 1abcdef
60 clear_quantities.append(qty) 1abcdef
61 else:
62 duplicate_quantities.append(qty._unreduced) 1abcdef
64 self._quantities = clear_quantities 1abcdef
66 if len(duplicate_quantities) > 0: 1abcdef
67 _show_nodimo_warning( 1abcdef
68 f"Duplicate quantities ({str(duplicate_quantities)[1:-1]})"
69 )
72class HomogeneousGroup(Group): 1abcdef
73 """Dimensionally homogeneous group of quantities.
75 Warns
76 -----
77 NodimoWarning
78 Removed dimensionally irrelevant quantities.
79 """
81 def __init__(self, *quantities: Quantity): 1abcdef
82 super().__init__(*quantities) 1abcdef
83 self._set_homogeneous_group() 1abcdef
85 def _set_homogeneous_group(self): 1abcdef
86 self._clear_heterogeneous_quantities() 1abcdef
88 def _clear_heterogeneous_quantities(self): 1abcdef
89 clear_quantities = list(self._quantities) 1abcdef
90 irrelevant_quantities = [] 1abcdef
91 for _ in self._quantities: 1abcdef
92 irr_qty = None 1abcdef
93 for dim in self._dimensions: 1abcdef
94 dim_bool = [] 1abcdef
95 for qty in clear_quantities: 1abcdef
96 if dim in qty.dimension: 1abcdef
97 dim_bool.append(True) 1abcdef
98 else:
99 dim_bool.append(False) 1abcdef
101 dim_count = sum(dim_bool) 1abcdef
102 if dim_count == 1: 1abcdef
103 irr_qty = clear_quantities[dim_bool.index(True)] 1abcdef
104 irrelevant_quantities.append(irr_qty._unreduced) 1abcdef
105 clear_quantities.remove(irr_qty) 1abcdef
106 self._quantities = clear_quantities 1abcdef
107 self._set_collection_dimensions() 1abcdef
108 break 1abcdef
109 if irr_qty is None: 1abcdef
110 break 1abcdef
112 if len(irrelevant_quantities) > 0: 1abcdef
113 _show_nodimo_warning( 1abcdef
114 f"Dimensionally irrelevant quantities "
115 f"({str(irrelevant_quantities)[1:-1]})"
116 )
119class ScalingGroup(Group): 1abcdef
120 """A Group of scaling quantities.
122 Raises
123 ------
124 ValueError
125 If the quantities are not dimensionally independent.
126 """
128 def __init__(self, *quantities: Quantity, id_number: int = 0): 1abcdef
129 super().__init__(*quantities) 1abcdef
130 self._id_number: int = id_number 1abcdef
131 self._set_scaling_group() 1abcdef
133 def _set_scaling_group(self): 1abcdef
134 self._clear_constants() 1abcdef
135 self._set_scaling_group_quantities() 1abcdef
136 self._set_matrix_rank() 1abcdef
137 self._validate_scaling_group() 1abcdef
139 def _set_scaling_group_quantities(self): 1abcdef
140 """Sets all quantities in the group as scaling."""
142 new_quantities = [] 1abcdef
143 for qty in self._quantities: 1abcdef
144 if qty.is_scaling: 1abcdef
145 new_qty = qty 1abcdef
146 else:
147 new_qty = qty._copy() 1abcdef
148 new_qty._is_scaling = True 1abcdef
149 new_qty._validate_quantity() 1abcdef
150 new_quantities.append(new_qty) 1abcdef
152 self._quantities = new_quantities 1abcdef
154 def _validate_scaling_group(self): 1abcdef
155 if len(self._quantities) > self._rank: 1abcdef
156 raise ValueError("Quantities are not dimensionally independent") 1abcdef
158 def _sympyrepr(self, printer) -> str: 1abcdef
159 class_name = type(self).__name__ 1abcdef
160 quantities = ', '.join( 1abcdef
161 printer._print(qty._unreduced) for qty in self._quantities
162 )
163 id_number = f', id_number={self._id_number}' if self._id_number else '' 1abcdef
165 return f'{class_name}({quantities}{id_number})' 1abcdef
167 def _sympystr(self, printer) -> str: 1abcdef
168 id_number = f' {self._id_number}' if self._id_number else '' 1abcdef
169 scgroup = printer._print(f"Scaling group{id_number} ") 1abcdef
170 quantities = ', '.join( 1abcdef
171 printer._print(qty._unreduced) for qty in self._quantities
172 )
174 return f'{scgroup}({quantities})' 1abcdef
176 def _latex(self, printer) -> str: 1abcdef
177 id_number = f' {self._id_number}' if self._id_number else '' 1abcdef
178 scgroup = printer._print(f"Scaling group{id_number} ") 1abcdef
179 quantities = R',\ '.join( 1abcdef
180 printer._print(qty._unreduced) for qty in self._quantities
181 )
183 return f'{scgroup}\\left({quantities}\\right)' 1abcdef
185 def _pretty(self, printer) -> prettyForm: 1abcdef
186 id_number = f' {self._id_number}' if self._id_number else '' 1abcdef
187 scgroup = printer._print(f"Scaling group{id_number} ") 1abcdef
189 quantities = prettyForm('') 1abcdef
190 for i, qty in enumerate(self._quantities): 1abcdef
191 sep = ', ' if i > 0 else '' 1abcdef
192 quantities = prettyForm( 1abcdef
193 *quantities.right(sep, printer._print(qty._unreduced))
194 )
195 quantities = prettyForm(*quantities.parens()) 1abcdef
197 return prettyForm(*scgroup.right(quantities)) 1abcdef
200class IndependentGroup(Group): 1abcdef
201 """Independent group of quantities.
203 A group is independent if any quantity in it cannot be obtained as
204 the product of powers of the other quantities of the group.
206 In the context of this class, all quantities are considered derived,
207 that is, all quantities are expressed as products of powers of the
208 base quantities of the group. This allow us to remove derived
209 quantities that are dependent on other derived quantities.
211 To find dependent derived quantities, they are defined in such a way
212 that their dimensions use the names of the base quantities as the
213 dimensions' names and the exponents of the base quantities as the
214 dimensions' exponents. The resulting dimensional matrix and its rank
215 will determine the independency status of the group.
217 Warns
218 -----
219 NodimoWarning
220 Removed dependent derived quantities.
222 Notes
223 -----
224 1. The terms 'dependent' and 'independent' employed here have
225 nothing to do with the quantity attribute ``is_dependent``
226 used to build relations.
227 2. The base quantities are not necessarily the quantities of the
228 group.
229 """
231 def __init__(self, *quantities: Quantity): 1abcdef
232 super().__init__(*quantities) 1abcdef
233 self._derived_quantities: list[Quantity] 1abcdef
234 self._set_independent_group() 1abcdef
236 def _set_independent_group(self): 1abcdef
237 self._clear_constants() 1abcdef
238 self._set_derived_quantities() 1abcdef
239 self._clear_dependent_derived_quantities() 1abcdef
241 def _get_derived_dimensions(self, quantity): 1abcdef
242 dimensions = {} 1abcdef
243 if quantity._is_product: 1abcdef
244 for qty in quantity.factors: 1abcdef
245 dimensions = {**dimensions, **self._get_derived_dimensions(qty)} 1abcdef
246 elif quantity._is_power: 1abcdef
247 dimensions[quantity.base.name] = quantity.exponent 1abcdef
248 elif not quantity._is_constant: 1abcdef
249 dimensions[quantity.name] = S.One 1abcdef
251 return dimensions 1abcdef
253 def _set_derived_quantities(self): 1abcdef
254 derived_quantities = {} 1abcdef
255 for i, qty in enumerate(self._quantities): 1abcdef
256 dimensions = self._get_derived_dimensions(qty) 1abcdef
257 derived_quantities[Quantity(f'dQ{i}', **dimensions)] = qty 1abcdef
259 self._derived_quantities = derived_quantities 1abcdef
261 def _clear_dependent_derived_quantities(self): 1abcdef
262 derived_group = Group(*self._derived_quantities) 1abcdef
263 derived_group._set_matrix_rank() 1abcdef
264 if len(self._derived_quantities) > derived_group._rank: 1abcdef
265 _, indep_qts_indexes = derived_group._matrix.rref() 1abcdef
266 else:
267 indep_qts_indexes = tuple(range(len(self._derived_quantities))) 1abcdef
269 independent_quantities = [] 1abcdef
270 dependent_quantities = [] 1abcdef
271 for i, qty in enumerate(self._quantities): 1abcdef
272 if i in indep_qts_indexes: 1abcdef
273 independent_quantities.append(qty) 1abcdef
274 else:
275 dependent_quantities.append(qty._unreduced) 1abcdef
277 self._quantities = independent_quantities 1abcdef
279 if len(dependent_quantities) > 0: 1abcdef
280 _show_nodimo_warning( 1abcdef
281 f"Dependent derived quantities ({str(dependent_quantities)[1:-1]})"
282 )
285class DimensionalGroup(HomogeneousGroup, IndependentGroup): 1abcdef
286 """Dimensional group of derived quantities.
288 This class creates a group of derived quantities from a homogeneous
289 and independent group of provided quantities. The derived quantities
290 are built as products of powers of the original quantities in a way
291 that all derived quantities contain the same requested dimension.
293 To achieve this, one must provide quantities with scaling property,
294 in a number that matches the rank of the original group. The scaling
295 quantities work as tranformation parameters that, combined with the
296 nonscaling quantities, produce derived quantities with the demanded
297 dimension. The ensuing number of derived quantities is always lower
298 or equal to the number of original quantities.
300 Parameters
301 ----------
302 *quantities : Quantity
303 Quantities to be transformed.
304 **dimensions : Number
305 Aimed dimensions for the group given as keyword arguments.
306 Dimensions that are not given are considered null.
308 Raises
309 ------
310 ValueError
311 If the group does not have the necessary number of dimensionally
312 independent scaling quantities.
314 Warns
315 -----
316 NodimoWarning
317 Dimensions that are treated as independent.
319 References
320 ----------
321 .. [1] Thomas Szirtes, Applied Dimensional Analysis and Modeling
322 (Butterworth-Heinemann, 2007), p. 133.
323 """
325 def __init__(self, *quantities: Quantity, **dimensions: Number): 1abcdef
326 super(DimensionalGroup, self).__init__(*quantities) 1abcdef
327 super(HomogeneousGroup, self).__init__(*self._quantities) 1abcdef
328 self._original_quantities: list[Quantity] = self._quantities 1abcdef
329 self._set_dimensions(**dimensions) 1abcdef
330 self._set_dimensional_group() 1abcdef
332 def _set_dimensional_group(self): 1abcdef
333 self._set_scaling_quantities() 1abcdef
334 self._set_scaling_matrix() 1abcdef
335 self._validate_dimensional_group() 1abcdef
336 self._set_exponents() 1abcdef
337 self._set_dimensional_group_quantities() 1abcdef
338 self._clear_duplicate_quantities() 1abcdef
339 self._clear_null_dimensions() 1abcdef
341 def _set_scaling_matrix(self): 1abcdef
342 scaling_matrix = self._get_submatrix(*self._scaling_quantities) 1abcdef
343 nonscaling_matrix = self._get_submatrix(*self._nonscaling_quantities) 1abcdef
345 self._scaling_matrix = scaling_matrix[self._independent_rows, :] 1abcdef
346 self._nonscaling_matrix = nonscaling_matrix[self._independent_rows, :] 1abcdef
348 def _validate_dimensional_group(self): 1abcdef
349 check1 = len(self._scaling_quantities) == self._rank 1abcdef
350 check2 = self._scaling_matrix.rank() == self._rank 1abcdef
351 if not check1 or not check2: 1abcdef
352 raise ValueError( 1abcdef
353 f"The group must have {self._rank} "
354 f"dimensionally independent scaling quantities"
355 )
357 def _set_exponents(self): 1abcdef
358 """Sets the exponents to build the products of powers."""
360 nqts = len(self._quantities) 1abcdef
361 rank = self._rank 1abcdef
362 hasdim = int(not self._is_dimensionless) 1abcdef
363 nnonsc = nqts - rank 1abcdef
364 nprods = nnonsc + hasdim 1abcdef
366 A = self._scaling_matrix 1abcdef
367 B = self._nonscaling_matrix 1abcdef
369 E11 = eye(nnonsc) 1abcdef
370 E12 = zeros(nnonsc, rank) 1abcdef
371 E21 = -(A**-1) * B 1abcdef
372 E22 = A**-1 1abcdef
373 E = Matrix([[E11, E12], [E21, E22]]) 1abcdef
375 Z1 = Matrix.hstack(eye(nnonsc), zeros(nnonsc, hasdim)) 1abcdef
376 Z2C = Matrix(list(self._independent_dimensions.values())) 1abcdef
377 Z2 = Matrix.hstack(*nprods * [Z2C]) 1abcdef
378 Z = Matrix([[Z1], [Z2]]) 1abcdef
380 P = E * Z 1abcdef
382 self._exponents = P.as_immutable() 1abcdef
384 def _set_dimensional_group_quantities(self): 1abcdef
385 quantities = self._nonscaling_quantities + self._scaling_quantities 1abcdef
386 products = [] 1abcdef
387 for j in range(self._exponents.cols): 1abcdef
388 factors = [] 1abcdef
389 for qty, exp in zip(quantities, self._exponents.col(j)): 1abcdef
390 factors.append(Power(qty, exp)) 1abcdef
391 products.append(Product(*factors)) 1abcdef
393 self._quantities = products 1abcdef
395 def _sympyrepr(self, printer) -> str: 1abcdef
396 class_name = type(self).__name__ 1abcdef
397 quantities = ', '.join( 1abcdef
398 printer._print(qty._unreduced) for qty in self._original_quantities
399 )
401 if self._is_dimensionless: 1abcdef
402 dimensions = '' 1abcdef
403 else:
404 dims = [] 1abcdef
405 for dim_name, dim_exp in self._dimensions.items(): 1abcdef
406 dim_exp_ = _unsympify_number(dim_exp) 1abcdef
407 if isinstance(dim_exp_, str): 1abcdef
408 dim_exp_ = f"'{dim_exp_}'" 1abcdef
409 dims.append(f'{dim_name}={dim_exp_}') 1abcdef
410 dimensions = f", {', '.join(dims)}" 1abcdef
412 return f'{class_name}({quantities}{dimensions})' 1abcdef