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

92 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-11-06 23:27 +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 IdaesSolveCompletionEvent, DispatchMultiSolveEvent 

6from core.auxiliary.serializers import TaskSerializer 

7from idaes_factory import endpoints 

8from pgraph_factory.pg_sheet import PgProcess 

9from drf_spectacular.utils import extend_schema 

10from rest_framework.response import Response 

11from rest_framework.decorators import api_view, authentication_classes 

12from rest_framework import serializers 

13from flowsheetInternals.unitops.models.SimulationObject import SimulationObject 

14from idaes_factory.endpoints import cancel_idaes_solve, process_idaes_solve_response, start_flowsheet_solve_event, \ 

15 start_multi_steady_state_solve_event, process_failed_idaes_solve_response 

16from idaes_factory.idaes_factory_context import LiveSolveParams 

17from core.validation import api_view_validate 

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

19 

20 

21class SolveRequestSerializer(serializers.Serializer): 

22 flowsheet_id = serializers.IntegerField(required=True) 

23 debug = serializers.BooleanField(required=False) 

24 require_variables_fixed = serializers.BooleanField(required=False) 

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

26 perform_diagnostics = serializers.BooleanField(required=False, default=False) # currently, this doesn't do anything on MSS solves. 

27 

28 

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

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

31 

32 Args: 

33 message: Human-readable description of the failure. 

34 cause: Short identifier describing which phase failed. 

35 

36 Returns: 

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

38 """ 

39 return Response(status=400, data={ 

40 "status": "error", 

41 "error": { 

42 "message": message, 

43 "cause": cause, 

44 "traceback": traceback.format_exc() 

45 }, 

46 "log": None, 

47 "debug": { 

48 "input_flowsheet": None, 

49 "output_flowsheet": None, 

50 "timing": {} 

51 } 

52 }) 

53 

54 

55@api_view_validate 

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

57@api_view(['POST']) 

58def solve_idaes(request) -> Response: 

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

60 # Validate the request data 

61 try: 

62 serializer = SolveRequestSerializer(data=request.data) 

63 serializer.is_valid(raise_exception=True) 

64 validated_data = serializer.validated_data 

65 

66 param_flowsheet = request.query_params.get('flowsheet') 

67 flowsheet_id = validated_data.get('flowsheet_id') 

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

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

70 

71 # since we don't check if user has access to the flowsheet_id, 

72 # we need to check if flowsheet id == one in request param 

73 # which is verified with the api_view_validate decorator 

74 if int(param_flowsheet) != int(flowsheet_id): 

75 return create_error(f"Flowsheet ID mismatch, {param_flowsheet} & {flowsheet_id}", "validation") 

76 

77 except Exception as e: 

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

79 

80 # Create the factory 

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

82 try: 

83 # get the optimisation that matches the flowsheet 

84 scenario = (Scenario.objects 

85 .filter(id=scenario_number) 

86 .first() 

87 ) 

88 

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

90 return start_multi_steady_state_solve_event(flowsheet_id, request.user, scenario) 

91 

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

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

94 

95 # Check the existence of a decision node 

96 if SimulationObject.objects.filter(objectType="decisionNode", flowsheet=flowsheet_id).count() == 0: 96 ↛ 100line 96 didn't jump to line 100 because the condition on line 96 was always true

97 # Run as normal 

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

99 else: 

100 pgraph_factory = PgProcess(flowsheet_id) 

101 pgraph_factory.solve() 

102 pgraph_factory.create_process_paths() 

103 

104 return Response(status=200, data=[[block.componentName for block in solution] for solution in pgraph_factory.solutions]) 

105 except Exception as e: 

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

107 

108@extend_schema(exclude=True) 

109@api_view(['POST']) 

110@authentication_classes([DaprApiTokenAuthentication]) 

111@csrf_exempt 

112def process_idaes_solve_completion_event(request) -> Response: 

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

114 solve_response = IdaesSolveCompletionEvent.model_validate(request.data) 

115 solve_data = solve_response.data 

116 

117 process_idaes_solve_response(solve_data) 

118 

119 return Response(status=200) 

120 

121@extend_schema(exclude=True) 

122@api_view(['POST']) 

123@authentication_classes([DaprApiTokenAuthentication]) 

124@csrf_exempt 

125def process_failed_idaes_solve_event(request) -> Response: 

126 """ 

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

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

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

130 """ 

131 solve_response = IdaesSolveCompletionEvent.model_validate(request.data) 

132 solve_data = solve_response.data 

133 

134 process_failed_idaes_solve_response(solve_data) 

135 

136 return Response(status=200) 

137 

138@extend_schema(exclude=True) 

139@api_view(['POST']) 

140@authentication_classes([DaprApiTokenAuthentication]) 

141@csrf_exempt 

142def process_dispatch_multi_solve(request) -> Response: 

143 """ 

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

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

146 """ 

147 

148 dispatch_request = DispatchMultiSolveEvent.model_validate(request.data) 

149 multi_solve_payload = dispatch_request.data 

150 

151 endpoints.dispatch_multi_solves(multi_solve_payload.task_id, multi_solve_payload.scenario_id) 

152 

153 return Response(status=200) 

154 

155class CancelTaskRequestSerializer(serializers.Serializer): 

156 task_id = serializers.IntegerField() 

157 

158@extend_schema(request=CancelTaskRequestSerializer) 

159@api_view_validate 

160@api_view(['POST']) 

161def cancel_idaes_solve_handler(request) -> Response: 

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

163 cancel_request_serializer = CancelTaskRequestSerializer(data=request.data) 

164 cancel_request_serializer.is_valid(raise_exception=True) 

165 cancel_request = cancel_request_serializer.validated_data 

166 

167 task_id = cancel_request.get('task_id') 

168 

169 # Need to add task_id query parameter 

170 cancel_idaes_solve(task_id) 

171 

172 return Response(status=200)