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
« 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
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.
29def create_error(message, cause) -> Response:
30 """Build a standardised error response payload for solve requests.
32 Args:
33 message: Human-readable description of the failure.
34 cause: Short identifier describing which phase failed.
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 })
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
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)
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")
77 except Exception as e:
78 return create_error("Invalid request data", "validation")
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 )
89 if scenario and scenario.state_name == ScenarioTabTypeEnum.MultiSteadyState:
90 return start_multi_steady_state_solve_event(flowsheet_id, request.user, scenario)
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.
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()
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")
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
117 process_idaes_solve_response(solve_data)
119 return Response(status=200)
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
134 process_failed_idaes_solve_response(solve_data)
136 return Response(status=200)
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 """
148 dispatch_request = DispatchMultiSolveEvent.model_validate(request.data)
149 multi_solve_payload = dispatch_request.data
151 endpoints.dispatch_multi_solves(multi_solve_payload.task_id, multi_solve_payload.scenario_id)
153 return Response(status=200)
155class CancelTaskRequestSerializer(serializers.Serializer):
156 task_id = serializers.IntegerField()
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
167 task_id = cancel_request.get('task_id')
169 # Need to add task_id query parameter
170 cancel_idaes_solve(task_id)
172 return Response(status=200)