Coverage for backend/django/core/auxiliary/views/SolveView.py: 88%

91 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-26 20:57 +0000

1import traceback 

2import time 

3from django.views.decorators.csrf import csrf_exempt 

4from authentication.custom_drf_authentication import DaprApiTokenAuthentication 

5from common.models.idaes.payloads.solve_request_schema import ( 

6 IdaesSolveCompletionEvent, 

7 DispatchMultiSolveEvent, 

8) 

9from core.auxiliary.serializers import TaskSerializer 

10from idaes_factory import endpoints 

11from pgraph_factory.pg_sheet import PgProcess 

12from drf_spectacular.utils import extend_schema 

13from rest_framework.response import Response 

14from rest_framework.decorators import api_view, authentication_classes 

15from rest_framework import serializers 

16from flowsheetInternals.unitops.models.SimulationObject import SimulationObject 

17from core.auxiliary.models.Flowsheet import Flowsheet 

18from idaes_factory.endpoints import ( 

19 cancel_idaes_solve, 

20 process_idaes_solve_response, 

21 start_flowsheet_solve_event, 

22 start_multi_steady_state_solve_event, 

23 process_failed_idaes_solve_response, 

24) 

25from idaes_factory.idaes_factory_context import LiveSolveParams 

26from core.validation import api_view_validate 

27from core.auxiliary.models.Scenario import Scenario, ScenarioTabTypeEnum 

28 

29 

30class SolveRequestSerializer(serializers.Serializer): 

31 group_id = serializers.IntegerField(required=True) 

32 debug = serializers.BooleanField(required=False) 

33 require_variables_fixed = serializers.BooleanField(required=False) 

34 scenario_number = serializers.IntegerField(required=False, allow_null=True) 

35 perform_diagnostics = serializers.BooleanField( 

36 required=False, default=False 

37 ) # currently, this doesn't do anything on MSS solves. 

38 

39 

40def create_error(message, cause) -> Response: 

41 """Build a standardised error response payload for solve requests. 

42 

43 Args: 

44 message: Human-readable description of the failure. 

45 cause: Short identifier describing which phase failed. 

46 

47 Returns: 

48 REST response with a 400 status code and diagnostic metadata. 

49 """ 

50 return Response( 

51 status=400, 

52 data={ 

53 "status": "error", 

54 "error": { 

55 "message": message, 

56 "cause": cause, 

57 "traceback": traceback.format_exc(), 

58 }, 

59 "log": None, 

60 "debug": {"input_flowsheet": None, "output_flowsheet": None, "timing": {}}, 

61 }, 

62 ) 

63 

64 

65@api_view_validate 

66@extend_schema(request=SolveRequestSerializer, responses=TaskSerializer) 

67@api_view(["POST"]) 

68def solve_idaes(request) -> Response: 

69 """Dispatch a solve request to either IDAES or the process-graph solver.""" 

70 # Validate the request data 

71 try: 

72 serializer = SolveRequestSerializer(data=request.data) 

73 serializer.is_valid(raise_exception=True) 

74 validated_data = serializer.validated_data 

75 

76 flowsheet_id = request.query_params.get('flowsheet') 

77 group_id = validated_data.get('group_id') 

78 scenario_number: int = validated_data.get('scenario_number') 

79 perform_diagnostics: bool = validated_data.get('perform_diagnostics', False) 

80 

81 except Exception as e: 

82 return create_error("Invalid request data", "validation") 

83 

84 # Create the factory 

85 # This is where the flowsheet should be loaded from the database 

86 try: 

87 # get the optimisation that matches the flowsheet 

88 scenario = Scenario.objects.filter(id=scenario_number).first() 

89 

90 if scenario and scenario.state_name == ScenarioTabTypeEnum.MultiSteadyState: 

91 return start_multi_steady_state_solve_event( 

92 flowsheet_id, request.user, scenario 

93 ) 

94 

95 # TODO: Start using is_optimization to determine if to use the optimisation or not. 

96 # Stop sending multiple optimisations to the solver, just the scenario one. 

97 

98 # Check the existence of a decision node 

99 if ( 99 ↛ 108line 99 didn't jump to line 108 because the condition on line 99 was always true

100 SimulationObject.objects.filter( 

101 objectType="decisionNode", flowsheet=flowsheet_id 

102 ).count() 

103 == 0 

104 ): 

105 # Run as normal 

106 return start_flowsheet_solve_event(flowsheet_id, group_id, request.user, scenario, perform_diagnostics=perform_diagnostics) 

107 else: 

108 pgraph_factory = PgProcess(flowsheet_id) 

109 pgraph_factory.solve() 

110 pgraph_factory.create_process_paths() 

111 

112 return Response( 

113 status=200, 

114 data=[ 

115 [block.componentName for block in solution] 

116 for solution in pgraph_factory.solutions 

117 ], 

118 ) 

119 except Exception as e: 

120 return create_error(str(e), "idaes_factory_run") 

121 

122 

123@extend_schema(exclude=True) 

124@api_view(["POST"]) 

125@authentication_classes([DaprApiTokenAuthentication]) 

126@csrf_exempt 

127def process_idaes_solve_completion_event(request) -> Response: 

128 """Handle a solve completion event (sent by Dapr) from the IDAES service.""" 

129 solve_response = IdaesSolveCompletionEvent.model_validate(request.data) 

130 solve_data = solve_response.data 

131 

132 process_idaes_solve_response(solve_data) 

133 

134 return Response(status=200) 

135 

136 

137@extend_schema(exclude=True) 

138@api_view(["POST"]) 

139@authentication_classes([DaprApiTokenAuthentication]) 

140@csrf_exempt 

141def process_failed_idaes_solve_event(request) -> Response: 

142 """ 

143 This endpoint is used to process solve completion events that were not received or processed 

144 by Django correctly. Errors could be due to crashes, reaching the message TTL, concurrency issues, etc. 

145 This will allow unprocessed solve tasks to be marked as failed and notify the user. 

146 """ 

147 solve_response = IdaesSolveCompletionEvent.model_validate(request.data) 

148 solve_data = solve_response.data 

149 

150 process_failed_idaes_solve_response(solve_data) 

151 

152 return Response(status=200) 

153 

154 

155@extend_schema(exclude=True) 

156@api_view(["POST"]) 

157@authentication_classes([DaprApiTokenAuthentication]) 

158@csrf_exempt 

159def process_dispatch_multi_solve(request) -> Response: 

160 """ 

161 This endpoint is used to process dispatch multi-solve events sent via the primary 

162 solve endpoint when the scenario is a multi-steady state scenario. 

163 """ 

164 

165 dispatch_request = DispatchMultiSolveEvent.model_validate(request.data) 

166 multi_solve_payload = dispatch_request.data 

167 

168 endpoints.dispatch_multi_solves( 

169 multi_solve_payload.task_id, multi_solve_payload.scenario_id 

170 ) 

171 

172 return Response(status=200) 

173 

174 

175class CancelTaskRequestSerializer(serializers.Serializer): 

176 task_id = serializers.IntegerField() 

177 

178 

179@extend_schema(request=CancelTaskRequestSerializer) 

180@api_view_validate 

181@api_view(["POST"]) 

182def cancel_idaes_solve_handler(request) -> Response: 

183 """Accept a client request to cancel a pending or running solve task.""" 

184 cancel_request_serializer = CancelTaskRequestSerializer(data=request.data) 

185 cancel_request_serializer.is_valid(raise_exception=True) 

186 cancel_request = cancel_request_serializer.validated_data 

187 

188 task_id = cancel_request.get("task_id") 

189 

190 # Need to add task_id query parameter 

191 cancel_idaes_solve(task_id) 

192 

193 return Response(status=200)