Coverage for nodimo / groups.py: 100%

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

6Groups 

7====== 

8 

9This module contains classes to create groups of quantities, which are 

10specific types of collections. 

11 

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

25 

26from sympy import Number, Matrix, zeros, eye, S 1abcdef

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

28from typing import Union 1abcdef

29 

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

35 

36 

37class Group(Collection): 1abcdef

38 """Group of quantities. 

39 

40 Equivalent to a Collection, but without duplicate quantities. 

41 

42 Warns 

43 ----- 

44 NodimoWarning 

45 Removed duplicate quantities. 

46 """ 

47 

48 def __init__(self, *quantities: Quantity): 1abcdef

49 super().__init__(*quantities) 1abcdef

50 self._set_group() 1abcdef

51 

52 def _set_group(self): 1abcdef

53 self._clear_duplicate_quantities() 1abcdef

54 

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

63 

64 self._quantities = clear_quantities 1abcdef

65 

66 if len(duplicate_quantities) > 0: 1abcdef

67 _show_nodimo_warning( 1abcdef

68 f"Duplicate quantities ({str(duplicate_quantities)[1:-1]})" 

69 ) 

70 

71 

72class HomogeneousGroup(Group): 1abcdef

73 """Dimensionally homogeneous group of quantities. 

74 

75 Warns 

76 ----- 

77 NodimoWarning 

78 Removed dimensionally irrelevant quantities. 

79 """ 

80 

81 def __init__(self, *quantities: Quantity): 1abcdef

82 super().__init__(*quantities) 1abcdef

83 self._set_homogeneous_group() 1abcdef

84 

85 def _set_homogeneous_group(self): 1abcdef

86 self._clear_heterogeneous_quantities() 1abcdef

87 

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

100 

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

111 

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 ) 

117 

118 

119class ScalingGroup(Group): 1abcdef

120 """A Group of scaling quantities. 

121 

122 Raises 

123 ------ 

124 ValueError 

125 If the quantities are not dimensionally independent. 

126 """ 

127 

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

132 

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

138 

139 def _set_scaling_group_quantities(self): 1abcdef

140 """Sets all quantities in the group as scaling.""" 

141 

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

151 

152 self._quantities = new_quantities 1abcdef

153 

154 def _validate_scaling_group(self): 1abcdef

155 if len(self._quantities) > self._rank: 1abcdef

156 raise ValueError("Quantities are not dimensionally independent") 1abcdef

157 

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

164 

165 return f'{class_name}({quantities}{id_number})' 1abcdef

166 

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 ) 

173 

174 return f'{scgroup}({quantities})' 1abcdef

175 

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 ) 

182 

183 return f'{scgroup}\\left({quantities}\\right)' 1abcdef

184 

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

188 

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

196 

197 return prettyForm(*scgroup.right(quantities)) 1abcdef

198 

199 

200class IndependentGroup(Group): 1abcdef

201 """Independent group of quantities. 

202 

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. 

205 

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. 

210 

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. 

216 

217 Warns 

218 ----- 

219 NodimoWarning 

220 Removed dependent derived quantities. 

221 

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

230 

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

235 

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

240 

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

250 

251 return dimensions 1abcdef

252 

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

258 

259 self._derived_quantities = derived_quantities 1abcdef

260 

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

268 

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

276 

277 self._quantities = independent_quantities 1abcdef

278 

279 if len(dependent_quantities) > 0: 1abcdef

280 _show_nodimo_warning( 1abcdef

281 f"Dependent derived quantities ({str(dependent_quantities)[1:-1]})" 

282 ) 

283 

284 

285class DimensionalGroup(HomogeneousGroup, IndependentGroup): 1abcdef

286 """Dimensional group of derived quantities. 

287 

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. 

292 

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. 

299 

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. 

307 

308 Raises 

309 ------ 

310 ValueError 

311 If the group does not have the necessary number of dimensionally 

312 independent scaling quantities. 

313 

314 Warns 

315 ----- 

316 NodimoWarning 

317 Dimensions that are treated as independent. 

318 

319 References 

320 ---------- 

321 .. [1] Thomas Szirtes, Applied Dimensional Analysis and Modeling 

322 (Butterworth-Heinemann, 2007), p. 133. 

323 """ 

324 

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

331 

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

340 

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

344 

345 self._scaling_matrix = scaling_matrix[self._independent_rows, :] 1abcdef

346 self._nonscaling_matrix = nonscaling_matrix[self._independent_rows, :] 1abcdef

347 

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 ) 

356 

357 def _set_exponents(self): 1abcdef

358 """Sets the exponents to build the products of powers.""" 

359 

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

365 

366 A = self._scaling_matrix 1abcdef

367 B = self._nonscaling_matrix 1abcdef

368 

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

374 

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

379 

380 P = E * Z 1abcdef

381 

382 self._exponents = P.as_immutable() 1abcdef

383 

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

392 

393 self._quantities = products 1abcdef

394 

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 ) 

400 

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

411 

412 return f'{class_name}({quantities}{dimensions})' 1abcdef