Coverage for nodimo / model.py: 100%

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

6Model 

7===== 

8 

9This module contains the classes to create (non)dimensional models. 

10 

11Classes 

12------- 

13Model 

14 Creates a (non)dimensional model from a given set of quantities. 

15""" 

16 

17from sympy import Number 1abcdef

18from itertools import combinations 1abcdef

19 

20from nodimo.quantity import Quantity 1abcdef

21from nodimo.groups import DimensionalGroup, ScalingGroup 1abcdef

22from nodimo.relation import Relation 1abcdef

23from nodimo._internal import _print_horizontal_line, _unsympify_number 1abcdef

24 

25 

26class Model(Relation): 1abcdef

27 """(Non)dimensional model built from a given set of quantities. 

28 

29 This class builds relations from the resulting quantities of a group 

30 transformation (DimensionalGroup). Every relation is associated with 

31 a scaling group, which are built using the given scaling quantities. 

32 

33 Parameters 

34 ---------- 

35 *quantities : Quantity 

36 Quantities that constitute the model. 

37 **dimensions : int 

38 Aimed dimensions for the model given as keyword arguments. 

39 

40 Attributes 

41 ---------- 

42 quantities : list[Quantity] 

43 List with the quantities that constitute the model. 

44 relations : dict[ScalingGroup, Relation] 

45 Dictionary containing pairs of scaling groups and relations. 

46 

47 Methods 

48 ------- 

49 show(use_custom_css=True, use_unicode=True) 

50 Prints scaling groups and respective (non)dimensional relations. 

51 

52 Raises 

53 ------ 

54 ValueError 

55 If the number of scaling quantities is lower than necessary. 

56 

57 Warns 

58 ----- 

59 NodimoWarning 

60 Dimensions that are treated as independent. 

61 

62 Examples 

63 -------- 

64 * Free fall 

65 

66 Dimensions: mass ``M``, length ``L`` and time ``T``. 

67 

68 Quantities: height ``z``, mass ``m``, velocity ``v``, 

69 gravitational acceleration ``g``, time ``t``, 

70 initial height ``z0`` and initial velocity ``v0`` 

71 

72 Considering the scaling groups that can be formed with ``g``, ``z0`` 

73 and ``v0``, the ``model`` for ``z`` is built and displayed as: 

74 

75 >>> from nodimo import Quantity, Model 

76 >>> z = Quantity('z', L=1, dependent=True) 

77 >>> m = Quantity('m', M=1) 

78 >>> v = Quantity('v', L=1, T=-1) 

79 >>> g = Quantity('g', L=1, T=-2, scaling=True) 

80 >>> t = Quantity('t', T=1) 

81 >>> z0 = Quantity('z0', L=1, scaling=True) 

82 >>> v0 = Quantity('v0', L=1, T=-1, scaling=True) 

83 >>> model = Model(z, m, v, g, t, z0, v0) 

84 >>> model.show() 

85 """ 

86 

87 def __init__(self, *quantities: Quantity, **dimensions: Number): 1abcdef

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

89 self._set_dimensions(**dimensions) 1abcdef

90 self._scaling_groups: list[ScalingGroup] 1abcdef

91 self._relations: dict[ScalingGroup, Relation] 1abcdef

92 self._set_model() 1abcdef

93 

94 @property 1abcdef

95 def relations(self) -> dict[ScalingGroup, Relation]: 1abcdef

96 return self._relations 1abcdef

97 

98 def _set_model(self): 1abcdef

99 self._set_matrix_rank() 1abcdef

100 self._set_scaling_quantities() 1abcdef

101 self._validate_model() 1abcdef

102 self._set_scaling_groups() 1abcdef

103 self._set_relations() 1abcdef

104 self._clear_null_dimensions() 1abcdef

105 

106 def _validate_model(self): 1abcdef

107 if len(self._scaling_quantities) < self._rank: 1abcdef

108 raise ValueError( 1abcdef

109 f"The model must have at least {self._rank} scaling quantities" 

110 ) 

111 

112 def _set_scaling_groups(self): 1abcdef

113 scaling_groups = [] 1abcdef

114 for scgroup in combinations(self._scaling_quantities, self._rank): 1abcdef

115 try: 1abcdef

116 scaling_groups.append(ScalingGroup(*scgroup)) 1abcdef

117 except: 1abcdef

118 pass 1abcdef

119 

120 if len(scaling_groups) > 1: 1abcdef

121 idnum = 1 1abcdef

122 for scgroup in scaling_groups: 1abcdef

123 scgroup._id_number = idnum 1abcdef

124 idnum += 1 1abcdef

125 

126 self._scaling_groups = scaling_groups 1abcdef

127 

128 def _set_relations(self): 1abcdef

129 relations = {} 1abcdef

130 for scgroup in self._scaling_groups: 1abcdef

131 quantities = list(self._nonscaling_quantities) 1abcdef

132 for qty in self._scaling_quantities: 1abcdef

133 if qty not in scgroup: 1abcdef

134 reset_qty = qty._copy() 1abcdef

135 reset_qty._is_scaling = False 1abcdef

136 quantities.append(reset_qty) 1abcdef

137 quantities.extend(scgroup.quantities) 1abcdef

138 

139 dgroup = DimensionalGroup(*quantities, **self._dimensions) 1abcdef

140 for prod in dgroup.quantities: 1abcdef

141 if prod._is_product: 1abcdef

142 prod._is_dependent = any(qty.is_dependent for qty in prod.factors) 1abcdef

143 

144 if len(self._scaling_groups) == 1: 1abcdef

145 relation_name = 'Phi' 1abcdef

146 else: 

147 relation_name = f'Phi_{scgroup._id_number}' 1abcdef

148 

149 relations[scgroup] = Relation(*dgroup.quantities, name=relation_name) 1abcdef

150 

151 self._relations = relations 1abcdef

152 

153 def show(self, use_custom_css: bool = True, use_unicode: bool = True): 1abcdef

154 super().show(use_custom_css=use_custom_css, use_unicode=use_unicode) 1abcdef

155 for i, (scgroup, relation) in enumerate(self._relations.items()): 1abcdef

156 _print_horizontal_line() 1abcdef

157 scgroup.show(use_custom_css=False, use_unicode=use_unicode) 1abcdef

158 relation.show(use_custom_css=use_custom_css, use_unicode=use_unicode) 1abcdef

159 

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

161 return ( 1abcdef

162 frozenset(self._scaling_quantities), 

163 frozenset(self._nonscaling_quantities), 

164 frozenset(self._dimensions.items()), 

165 ) 

166 

167 def _sympyrepr(self, printer) -> str: 1abcdef

168 class_name = type(self).__name__ 1abcdef

169 quantities = ', '.join(printer._print(qty) for qty in self._quantities) 1abcdef

170 

171 if self._is_dimensionless: 1abcdef

172 dimensions = '' 1abcdef

173 else: 

174 dims = [] 1abcdef

175 for dim_name, dim_exp in self._dimensions.items(): 1abcdef

176 dim_exp_ = _unsympify_number(dim_exp) 1abcdef

177 if isinstance(dim_exp_, str): 1abcdef

178 dim_exp_ = f"'{dim_exp_}'" 1abcdef

179 dims.append(f'{dim_name}={dim_exp_}') 1abcdef

180 dimensions = f", {', '.join(dims)}" 1abcdef

181 

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