Coverage for backend/django/core/auxiliary/viewsets/compound_conversions.py: 71%

129 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-05-13 02:47 +0000

1from ahuora_compounds import CompoundDB 

2from ..models.PropertySet import PropertySet 

3from ..models.PropertyInfo import PropertyInfo 

4from ..models.PropertyValue import PropertyValue 

5from flowsheetInternals.unitops.config.objects.stream_config import property_combinations 

6 

7 

8def compound_db_to_molar_flow(name: str, value: float) -> float: 

9 """ 

10 convert a value as mass flow to molar flow 

11 (assumes converting kg/s to mol/s) 

12 """ 

13 compound = CompoundDB.get_compound(name) 

14 molecular_weight: float = compound.MolecularWeight.value # g/mol 

15 return value / molecular_weight * 1000 

16 

17 

18def compound_db_to_mass_flow(name: str, value: float) -> float: 

19 """ 

20 convert a value as molar flow to mass flow 

21 (assumes converting mol/s to kg/s) 

22 """ 

23 compound = CompoundDB.get_compound(name) 

24 molecular_weight: float = compound.MolecularWeight.value # g/mol 

25 return value * molecular_weight / 1000 

26 

27 

28def update_fraction_display_values(property_set: PropertySet) -> None: 

29 """ 

30 Updates the display values of mass fractions based on the raw values 

31 """ 

32 # Get the molar fraction property and its values 

33 mole_frac_comp = property_set.get_property("mole_frac_comp") 

34 property_values = mole_frac_comp.values.all() 

35 

36 # Calculate mass for each compound and track total mass 

37 compound_masses = [] 

38 total_mass = 0.0 

39 

40 for prop in property_values: 

41 # Get molar fraction (raw value) 

42 molar_fraction = float(prop.value) if prop.value not in [None, ""] else 0.0 

43 

44 # Get molecular weight and calculate mass 

45 compound_name = prop.get_index("compound").key 

46 compound = CompoundDB.get_compound(compound_name) 

47 molecular_weight = compound.MolecularWeight.value # g/mol 

48 

49 # Calculate mass (molar fraction * molecular weight) 

50 mass = molar_fraction * molecular_weight 

51 compound_masses.append(mass) 

52 total_mass += mass 

53 

54 # Calculate and set mass fractions as display values 

55 if total_mass > 0: 

56 for i, prop in enumerate(property_values): 

57 # Calculate mass fraction 

58 mass_fraction = compound_masses[i] / total_mass 

59 # Set display value 

60 prop.displayValue = str(mass_fraction) 

61 

62 # Update in database 

63 PropertyValue.objects.bulk_update(property_values, ["displayValue"]) 

64 

65 

66def check_fully_defined( 

67 property_set: PropertySet, 

68 property_infos: list[PropertyInfo] | None = None, 

69 exclude: PropertyInfo | None = None, 

70 check_none_empty = False, 

71 check_fraction_sum = False 

72 ) -> bool: 

73 """ 

74 Returns true if the properties in the given property sets are 

75 fully defined (no read-write properties). 

76 """ 

77 if property_infos is None: 

78 property_infos = property_set.containedProperties.all() 

79 

80 for prop in property_infos: 

81 if ( 

82 check_none_empty 

83 and prop.key in ["flow_mol", "flow_mass", "flow_vol"] 

84 and not prop.has_value() 

85 ): 

86 return False 

87 if exclude is not None and prop.id == exclude.id: 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 continue 

89 if any([ 

90 property_value.is_enabled() and 

91 not property_value.has_value() 

92 for property_value in prop.values.all() 

93 ]): 

94 return False 

95 if ( 

96 check_fraction_sum 

97 and property_set.compoundMode in ["MolarFraction", "MassFraction"] 

98 ): 

99 mole_frac_comp = property_set.get_property("mole_frac_comp") 

100 return abs( 

101 sum([ 

102 ( 

103 float(prop.displayValue) 

104 if prop.displayValue not in [None, ""] 

105 else float(prop.value) 

106 if prop.value not in [None, ""] 

107 else 0 

108 ) 

109 for prop in mole_frac_comp.values.all() 

110 ]) - 1 

111 ) <= 1e-3 # sum of fractions == 1 

112 

113 return True 

114 

115 

116def serialize_to_current_mode(property_set: PropertySet, properties_schema: dict) -> None: 

117 """ 

118 Serialize the composition property set to the current compound mode 

119 (for GET requests). Adjusts the properties_schema in place 

120 """ 

121 def apply_display_value(property_key: str) -> None: 

122 property_schema = next( 

123 (item for item in properties_schema if item["key"] == property_key), 

124 None, 

125 ) 

126 if property_schema is None: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true

127 return 

128 

129 for value_data in property_schema["values"]: 

130 if value_data["displayValue"] not in [None, ""]: 

131 value_data["value"] = value_data["displayValue"] 

132 

133 mole_frac_comp_schema = next( 

134 (item for item in properties_schema if item["key"] == "mole_frac_comp"), 

135 None 

136 ) 

137 if mole_frac_comp_schema is None: 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true

138 return 

139 

140 if property_set.compoundMode == "MassFraction": 

141 apply_display_value("flow_mass") 

142 

143 if not stream_has_build_state_inputs(property_set): 

144 # stream is not fully defined, do not convert 

145 return 

146 

147 def iter_value_entries(): 

148 values = mole_frac_comp_schema["values"] 

149 if isinstance(values, dict): 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true

150 for key, value_data in values.items(): 

151 yield key, value_data 

152 return 

153 

154 for value_data in values: 

155 yield value_data["indexedSets"][0], value_data 

156 

157 def convert_to_mass_flow(): 

158 for key, value_data in iter_value_entries(): 

159 # convert to mass flow 

160 if value_data["value"] not in [None, ""]: 160 ↛ 158line 160 didn't jump to line 158 because the condition on line 160 was always true

161 value_data["value"] = compound_db_to_mass_flow(key, float(value_data["value"])) 

162 

163 def convert_to_mass_fraction(): 

164 # sums for molar fractions and mass fractions should be equal 

165 sum_molar_frac = 0 

166 for _, value_data in iter_value_entries(): 

167 sum_molar_frac += float(value_data["value"]) 

168 convert_to_mass_flow() 

169 sum_mass_flow = 0 

170 for _, value_data in iter_value_entries(): 

171 sum_mass_flow += float(value_data["value"]) 

172 

173 if sum_mass_flow == 0: 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

174 return 

175 for _, value_data in iter_value_entries(): 

176 value_data["value"] = float(value_data["value"]) / sum_mass_flow * sum_molar_frac 

177 

178 match property_set.compoundMode: 

179 case "MolarFraction": 179 ↛ 180line 179 didn't jump to line 180 because the pattern on line 179 never matched

180 pass # already in molar fractions 

181 case "MassFraction": 181 ↛ exitline 181 didn't return from function 'serialize_to_current_mode' because the pattern on line 181 always matched

182 convert_to_mass_fraction() 

183 

184 

185def convert_to_molar_fractions(property_set: PropertySet) -> None: 

186 """ 

187 Converts the composition to molar fractions from raw values 

188 """ 

189 mole_frac_comp = property_set.get_property("mole_frac_comp") 

190 property_values = mole_frac_comp.values.all() 

191 

192 def molar_flows_to_fractions() -> None: 

193 # convert from molar flows to molar fractions 

194 sum_flows = sum([float(prop.value) for prop in property_values]) 

195 if sum_flows == 0: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true

196 return 

197 for prop in property_values: 

198 prop.value = float(prop.value) / sum_flows 

199 

200 def mass_flows_to_molar_flows() -> None: 

201 # convert from mass flows to molar flows 

202 for prop in property_values: 

203 if prop.displayValue in [None, ""]: 

204 prop.displayValue = prop.value 

205 prop.value = compound_db_to_molar_flow(prop.get_index("compound").key, float(prop.displayValue)) 

206 

207 match property_set.compoundMode: 

208 case "MolarFraction": 208 ↛ 209line 208 didn't jump to line 209 because the pattern on line 208 never matched

209 return # already in molar fractions 

210 case "MassFraction": 210 ↛ 215line 210 didn't jump to line 215 because the pattern on line 210 always matched

211 # assume total mass flow of 1 

212 mass_flows_to_molar_flows() 

213 molar_flows_to_fractions() 

214 

215 PropertyValue.objects.bulk_update(property_values, ["value", "displayValue"]) 

216 

217 

218def convert_to_raw_values(property_set: PropertySet) -> None: 

219 """ 

220 Converts the composition to raw values from molar fractions 

221 """ 

222 mole_frac_comp = property_set.get_property("mole_frac_comp") 

223 property_values = mole_frac_comp.values.all() 

224 for prop in property_values: 

225 if prop.displayValue not in [None, ""]: 

226 prop.value = float(prop.displayValue) 

227 

228 PropertyValue.objects.bulk_update(property_values, ["value", "displayValue"]) 

229 

230 

231def stream_has_build_state_inputs(property_set: PropertySet) -> bool: 

232 """Return whether a stream has enough inputs for an async build-state.""" 

233 try: 

234 mole_frac_comp = property_set.get_property("mole_frac_comp") 

235 except ValueError: 

236 return False 

237 

238 has_complete_composition = check_fully_defined( 

239 property_set, 

240 [mole_frac_comp], 

241 check_fraction_sum=True, 

242 ) 

243 if not has_complete_composition: 

244 return False 

245 

246 for property_pair in property_combinations: 

247 if all(property_set.get_property(key).has_value() for key in property_pair): 

248 return True 

249 

250 return False