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
« 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
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.
40def create_error(message, cause) -> Response:
41 """Build a standardised error response payload for solve requests.
43 Args:
44 message: Human-readable description of the failure.
45 cause: Short identifier describing which phase failed.
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 )
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
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)
81 except Exception as e:
82 return create_error("Invalid request data", "validation")
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()
90 if scenario and scenario.state_name == ScenarioTabTypeEnum.MultiSteadyState:
91 return start_multi_steady_state_solve_event(
92 flowsheet_id, request.user, scenario
93 )
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.
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()
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")
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
132 process_idaes_solve_response(solve_data)
134 return Response(status=200)
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
150 process_failed_idaes_solve_response(solve_data)
152 return Response(status=200)
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 """
165 dispatch_request = DispatchMultiSolveEvent.model_validate(request.data)
166 multi_solve_payload = dispatch_request.data
168 endpoints.dispatch_multi_solves(
169 multi_solve_payload.task_id, multi_solve_payload.scenario_id
170 )
172 return Response(status=200)
175class CancelTaskRequestSerializer(serializers.Serializer):
176 task_id = serializers.IntegerField()
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
188 task_id = cancel_request.get("task_id")
190 # Need to add task_id query parameter
191 cancel_idaes_solve(task_id)
193 return Response(status=200)