Coverage for backend/django/core/auxiliary/views/ExtractSegmentDataFromFS.py: 87%

133 statements  

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

1import traceback 

2from typing import Union, Tuple 

3from rest_framework.decorators import api_view 

4from rest_framework.response import Response 

5from drf_spectacular.utils import extend_schema 

6from rest_framework import serializers, status 

7from idaes_factory.unit_conversion.unit_conversion import convert_value 

8from core.validation import api_view_validate 

9from core.managers import get_flowsheet_access 

10 

11from PinchAnalysis.models.InputModels import Segment, StreamDataEntry 

12from flowsheetInternals.unitops.services.edit_operations.recorder import ( 

13 tracked_bulk_create, 

14) 

15from PinchAnalysis.models.HenNode import HenNode 

16from PinchAnalysis.models.StreamDataProject import StreamDataProject 

17from PinchAnalysis.views.henNodeHelpers import group_stream_by_unitop_type, set_hennode_connections 

18from flowsheetInternals.unitops.models.SimulationObject import SimulationObject 

19from core.auxiliary.enums.pinchEnums import StreamType 

20from core.auxiliary.enums import SimulationObjectClass 

21from pinch_factory.pinch_factory import PinchFactory 

22from django.db.models import Q 

23 

24DECIMAL_PLACES = 3 

25VARIANCE = 0.01 

26 

27 

28def get_compounds(stream, include_null: bool = False) -> set[tuple[str, float]]: 

29 """ 

30 Returns a set of tuples containing the index (key) and value of all property 

31 value objects in the stream's mole_frac_comp. Used for composition comparisons. 

32 

33 Returns: 

34 - set[tuple[str, float]]: A set of (key, value) tuples. 

35 """ 

36 mole_frac_comp = stream.properties.get_property("mole_frac_comp") 

37 property_values = mole_frac_comp.values.all() 

38 result = [ 

39 (prop.get_index("compound").key, prop.value) 

40 for prop in property_values 

41 if (prop.value not in [None, ""] or include_null) 

42 ] 

43 return result 

44 

45 

46def create_he_streams(sim_obj, group) -> None: 

47 streamDataProject = group.flowsheet_state.StreamDataProject 

48 stream_ls: list[StreamDataEntry] = [] 

49 

50 for key in sim_obj.schema.propertyPackagePorts.keys(): 

51 if key != "__none__": 51 ↛ 50line 51 didn't jump to line 50 because the condition on line 51 was always true

52 stream_ls.append( 

53 StreamDataEntry( 

54 flowsheet_state=group.flowsheet_state, 

55 streamDataProject=streamDataProject, 

56 unitop=sim_obj, 

57 group=group, 

58 property_package_mapping=key, 

59 ) 

60 ) 

61 tracked_bulk_create(StreamDataEntry.objects, stream_ls) 

62 

63 

64def compare_compositions(stream_1: SimulationObject, stream_2: SimulationObject) -> bool: 

65 """ 

66 Compares the composition of two streams 

67 Returns: boolean indicating equality - True if compositions are equal 

68 """ 

69 stream_1_set = get_compounds(stream_1) 

70 stream_2_set = get_compounds(stream_2) 

71 return stream_1_set == stream_2_set 

72 

73# This needs to be revised and maybe moved elsewhere. The dT is WRONG. also it needs to be ln(dT). 

74# specifically, we need dT of cold segment, and dT of hot segment (pairs from exchanger(?), and we dont have that ehre.) 

75def _calc_area(htc: float, q_kw: float, t_supply_c: float, t_target_c: float) -> float: 

76 U = float(htc or 0) 

77 Q = float(q_kw or 0) 

78 dT = abs(float(t_supply_c or 0) - float(t_target_c or 0)) 

79 if U <= 0 or dT <= 0: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true

80 return 0.0 

81 return Q / (U * dT) 

82 

83 

84def _get_io_stream_properties(streamDataEntry: StreamDataEntry, prop_arg: str, tar_unit: str) -> Tuple[float,float]: 

85 i, o = getattr(streamDataEntry, prop_arg) 

86 i_val = i.get_value() 

87 o_val = o.get_value() 

88 if i_val is None or o_val is None: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true

89 return None, None 

90 else: 

91 supply = convert_value(i_val, i.unit, tar_unit) 

92 target = convert_value(o_val, o.unit, tar_unit) 

93 return supply, target 

94 

95 

96def _get_stream_type(streamDataEntry: StreamDataEntry): 

97 if streamDataEntry.unitop.objectType == SimulationObjectClass.HeatExchanger: 

98 if streamDataEntry.property_package_mapping == "Cold Side": 

99 stream_type = StreamType.Cold.value 

100 else: 

101 stream_type = StreamType.Hot.value 

102 else: 

103 if streamDataEntry.unitop.objectType == SimulationObjectClass.Heater: 

104 stream_type = StreamType.Cold.value 

105 else: 

106 stream_type = StreamType.Hot.value 

107 return stream_type 

108 

109 

110def _get_terminal_states(streamDataEntry: StreamDataEntry) -> dict: 

111 inlet_stream, _ = streamDataEntry.inlet_outlet_stream 

112 if not inlet_stream: 112 ↛ 113line 112 didn't jump to line 113 because the condition on line 112 was never true

113 return None 

114 

115 mole_flow = convert_value( 

116 inlet_stream.properties.get_property("flow_mol").get_value(), 

117 inlet_stream.properties.get_property("flow_mol").unit, 

118 "mol/s", 

119 ) 

120 service_T_supply, service_T_target = _get_io_stream_properties(streamDataEntry, "temperatures", "degK") 

121 service_P_supply, service_P_target = _get_io_stream_properties(streamDataEntry, "pressures", "Pa") 

122 service_H_supply, service_H_target = _get_io_stream_properties(streamDataEntry, "enthalpies", "J/mol") 

123 

124 if service_T_supply == None or service_P_supply == None or service_H_target == None: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true

125 return None 

126 else: 

127 return { 

128 "streams_io_props": [{ 

129 "t_supply": service_T_supply, 

130 "t_target": service_T_target, 

131 "p_supply": service_P_supply, 

132 "p_target": service_P_target, 

133 "h_supply": service_H_supply, 

134 "h_target": service_H_target, 

135 "composition": get_compounds(inlet_stream), 

136 }], 

137 "ppKey": streamDataEntry.unitop.get_property_package(), 

138 "mole_flow": mole_flow, 

139 "comp_name": inlet_stream.componentName, 

140 "stream_type": _get_stream_type(streamDataEntry), 

141 "streamDataEntry": streamDataEntry, 

142 } 

143 

144 

145def _stream_segment_creator(comp_name: str, points: list, stream_type: str, streamDataEntry: StreamDataEntry, **_): 

146 # Create segments after linearization 

147 segments = [] 

148 for index in range(len(points) - 1): 

149 t_supply = convert_value(points[index][1], "degK", "degC") 

150 t_target = check_target_temperature_validity( 

151 t_supply, 

152 convert_value(points[index + 1][1], "degK", "degC"), 

153 stream_type 

154 ) 

155 heat_flow = abs( 

156 convert_value(points[index + 1][0] - points[index][0], "W", "kW") 

157 ) 

158 htc=1 

159 segments.append( 

160 Segment( 

161 stream_data_entry=streamDataEntry, 

162 name=f"{comp_name} ({index + 1})" if len(points) > 1 else comp_name, 

163 t_supply=t_supply, 

164 t_target=t_target, 

165 heat_flow=heat_flow, 

166 htc=htc, 

167 area=_calc_area(htc,heat_flow, t_supply, t_target), 

168 flowsheet_state=streamDataEntry.flowsheet_state, 

169 ) 

170 ) 

171 return segments 

172 

173 

174@api_view_validate 

175@api_view(['POST']) 

176def extract_stream_data(request) -> Response: 

177 try: 

178 flowsheet_id = request.GET.get("flowsheet") 

179 access_state = get_flowsheet_access(request.user, flowsheet_id) 

180 if access_state.has_read_access and not access_state.has_write_access: 

181 return Response( 

182 {"error": "This flowsheet is shared with read-only access."}, 

183 status=status.HTTP_403_FORBIDDEN, 

184 ) 

185 factory = PinchFactory(flowsheet_id) 

186 

187 streamDataEntries = StreamDataEntry.objects.filter( 

188 flowsheet_state__flowsheet_id=flowsheet_id, 

189 custom=False, 

190 ) 

191 

192 segments = [] 

193 # i'll just delete hennodes and create new ones for now 

194 HenNode.objects.filter( 

195 flowsheet_state__flowsheet_id=flowsheet_id 

196 ).delete() 

197 

198 for streamDataEntry in streamDataEntries: 

199 streamDataEntry.Segments.all().delete() 

200 terminal_data = _get_terminal_states(streamDataEntry) 

201 if terminal_data is None: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 continue 

203 terminal_data["prev_states"] = streamDataEntry.states if hasattr(streamDataEntry, "states") else None 

204 t_h_data = factory.run_get_t_h_data(**terminal_data) 

205 

206 streamDataEntry.t_h_data = t_h_data["curve_points"] 

207 streamDataEntry.states = t_h_data["states"] 

208 streamDataEntry.save() 

209 

210 linearised_points = factory.run_linearize(t_h_data["curve_points"], **terminal_data) 

211 new_segments = _stream_segment_creator(points=linearised_points, **terminal_data) 

212 segments.extend(new_segments) 

213 

214 tracked_bulk_create(Segment.objects, segments) 

215 

216 # create hennodes 

217 created_segments = list(Segment.objects.filter(id__in=[s.id for s in segments])) 

218 sdes = {seg.stream_data_entry for seg in created_segments if seg.stream_data_entry} 

219 

220 processed_stream_ids = set(HenNode.objects.filter( 

221 Q(stream_data_entry__in=sdes) | 

222 Q(hot_connection__in=sdes) | 

223 Q(cold_connection__in=sdes) 

224 ).values_list('stream_data_entry_id', flat=True)) 

225 

226 hennodes_to_create = [] 

227 

228 # group streams by unitop type 

229 grouped_by_unitop = group_stream_by_unitop_type(sdes, processed_stream_ids, hennodes_to_create) 

230 

231 # set connections for the hennodes 

232 set_hennode_connections(grouped_by_unitop, processed_stream_ids, hennodes_to_create) 

233 

234 HenNode.objects.bulk_create(hennodes_to_create) 

235 

236 # link segments to hennodes 

237 for segment in created_segments: 

238 sde = segment.stream_data_entry 

239 if sde: 239 ↛ 237line 239 didn't jump to line 237 because the condition on line 239 was always true

240 hn = HenNode.objects.filter( 

241 Q(stream_data_entry=sde) | 

242 Q(hot_connection=sde) | 

243 Q(cold_connection=sde) 

244 ).first() 

245 if hn: 245 ↛ 237line 245 didn't jump to line 237 because the condition on line 245 was always true

246 segment.hen_node = hn 

247 segment.save(update_fields=['hen_node']) 

248 

249 return Response(status=200) 

250 

251 except Exception as e: 

252 tb_info = traceback.format_exc() 

253 print(tb_info) 

254 error_message = str(e) 

255 response_data = { 

256 "status": "error", 

257 "message": error_message, 

258 "traceback": tb_info 

259 } 

260 return Response(response_data, status=500) 

261 

262def check_target_temperature_validity(t_supply: float, t_target: float, stream_type: StreamType, min_delta_t: float = 0.0001) -> tuple[float, float]: 

263 if abs(t_supply - t_target) < min_delta_t: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true

264 t_target = t_supply - min_delta_t if stream_type == StreamType.Hot.value else t_supply + min_delta_t 

265 return t_target