Skip to content

Commit 04b8dc2

Browse files
authored
cuopt service correct definition for status value in lp result spec (#671)
The openapi spec for the status value in lp/mip results says "int" but it is actually a string. This change corrects that. Unfortunately, the enums defining the values cannot be directly imported because the import causes early cuda initialization which leads to rmm errors. So, this change uses local copies and then adds a unit test to make sure they don't drift. ## Summary by CodeRabbit ## Release Notes * **New Features** * Termination status values in API responses are now human-readable strings (e.g., "Optimal", "Infeasible") instead of numeric codes. * Added validation to ensure termination status values are valid. * **Tests** * Added verification test for termination status enumerations synchronization. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> Authors: - Trevor McKay (https://github.com/tmckayus) Approvers: - Ramakrishnap (https://github.com/rgsl888prabhu) URL: #671
1 parent 465f89f commit 04b8dc2

File tree

2 files changed

+101
-23
lines changed

2 files changed

+101
-23
lines changed

python/cuopt_server/cuopt_server/tests/test_lp.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,31 @@ def test_barrier_solver_options(
211211
res.json()["response"]["solver_response"],
212212
LPTerminationStatus.Optimal.name,
213213
)
214+
215+
216+
def test_termination_status_enum_sync():
217+
"""
218+
Ensure local status values in data_definition.py stay in sync
219+
with actual LPTerminationStatus and MILPTerminationStatus enums.
220+
221+
The data_definition module cannot import these enums directly
222+
because it triggers CUDA/RMM initialization before the server
223+
has configured memory management. So we maintain local copies
224+
and this test ensures they stay in sync.
225+
"""
226+
from cuopt_server.utils.linear_programming.data_definition import (
227+
LP_STATUS_NAMES,
228+
MILP_STATUS_NAMES,
229+
)
230+
231+
expected_lp = {e.name for e in LPTerminationStatus}
232+
expected_milp = {e.name for e in MILPTerminationStatus}
233+
234+
assert LP_STATUS_NAMES == expected_lp, (
235+
f"LP_STATUS_NAMES out of sync with LPTerminationStatus enum. "
236+
f"Expected: {expected_lp}, Got: {LP_STATUS_NAMES}"
237+
)
238+
assert MILP_STATUS_NAMES == expected_milp, (
239+
f"MILP_STATUS_NAMES out of sync with MILPTerminationStatus enum. "
240+
f"Expected: {expected_milp}, Got: {MILP_STATUS_NAMES}"
241+
)

python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -810,27 +810,77 @@ class SolutionData(StrictModel):
810810
)
811811

812812

813+
# LP termination status values
814+
# NOTE: These must match LPTerminationStatus from
815+
# cuopt.linear_programming.solver.solver_wrapper
816+
# We cannot import them directly because it triggers CUDA/RMM initialization
817+
# before the server has configured memory management.
818+
# See test_termination_status_enum_sync() in test_lp.py to ensure these stay in sync.
819+
LP_STATUS_NAMES = frozenset(
820+
{
821+
"NoTermination",
822+
"NumericalError",
823+
"Optimal",
824+
"PrimalInfeasible",
825+
"DualInfeasible",
826+
"IterationLimit",
827+
"TimeLimit",
828+
"PrimalFeasible",
829+
}
830+
)
831+
832+
# MILP termination status values
833+
# NOTE: These must match MILPTerminationStatus from
834+
# cuopt.linear_programming.solver.solver_wrapper
835+
MILP_STATUS_NAMES = frozenset(
836+
{
837+
"NoTermination",
838+
"Optimal",
839+
"FeasibleFound",
840+
"Infeasible",
841+
"Unbounded",
842+
"TimeLimit",
843+
}
844+
)
845+
846+
# Combined set of all valid status names
847+
ALL_STATUS_NAMES = LP_STATUS_NAMES | MILP_STATUS_NAMES
848+
849+
850+
def validate_termination_status(v):
851+
"""Validate that status is a valid LP or MILP termination status name."""
852+
if v not in ALL_STATUS_NAMES:
853+
raise ValueError(
854+
f"status must be one of {sorted(ALL_STATUS_NAMES)}, got '{v}'"
855+
)
856+
return v
857+
858+
813859
class SolutionResultData(StrictModel):
814-
status: int = Field(
815-
default=0,
816-
examples=[1],
817-
description=(
818-
"In case of LP : \n\n"
819-
"0 - No Termination \n\n"
820-
"1 - Optimal solution is available \n\n"
821-
"2 - Primal Infeasible solution \n\n"
822-
"3 - Dual Infeasible solution \n\n"
823-
"4 - Iteration Limit reached \n\n"
824-
"5 - TimeLimit reached \n\n"
825-
"6 - Primal Feasible \n\n"
826-
"---------------------- \n\n"
827-
"In case of MILP/IP : \n\n"
828-
"0 - No Termination \n\n"
829-
"1 - Optimal solution is available \n\n"
830-
"2 - Feasible solution is available \n\n"
831-
"3 - Infeasible \n\n"
832-
"4 - Unbounded\n\n"
833-
),
860+
status: Annotated[str, PlainValidator(validate_termination_status)] = (
861+
Field(
862+
default="NoTermination",
863+
examples=["Optimal"],
864+
description=(
865+
"In case of LP : \n\n"
866+
"NoTermination - No Termination \n\n"
867+
"NumericalError - Numerical Error \n\n"
868+
"Optimal - Optimal solution is available \n\n"
869+
"PrimalInfeasible - Primal Infeasible solution \n\n"
870+
"DualInfeasible - Dual Infeasible solution \n\n"
871+
"IterationLimit - Iteration Limit reached \n\n"
872+
"TimeLimit - TimeLimit reached \n\n"
873+
"PrimalFeasible - Primal Feasible \n\n"
874+
"---------------------- \n\n"
875+
"In case of MILP/IP : \n\n"
876+
"NoTermination - No Termination \n\n"
877+
"Optimal - Optimal solution is available \n\n"
878+
"FeasibleFound - Feasible solution is available \n\n"
879+
"Infeasible - Infeasible \n\n"
880+
"Unbounded - Unbounded \n\n"
881+
"TimeLimit - TimeLimit reached \n\n"
882+
),
883+
)
834884
)
835885
solution: SolutionData = Field(
836886
default=SolutionData(), description=("Solution of the LP problem")
@@ -896,7 +946,7 @@ class IncumbentSolution(StrictModel):
896946
"value": {
897947
"response": {
898948
"solver_response": {
899-
"status": 1,
949+
"status": "Optimal",
900950
"solution": {
901951
"problem_category": 0,
902952
"primal_solution": [0.0, 0.0],
@@ -925,7 +975,7 @@ class IncumbentSolution(StrictModel):
925975
"value": {
926976
"response": {
927977
"solver_response": {
928-
"status": 2,
978+
"status": "FeasibleFound",
929979
"solution": {
930980
"problem_category": 1,
931981
"primal_solution": [0.0, 0.0],
@@ -956,7 +1006,7 @@ class IncumbentSolution(StrictModel):
9561006
"value": {
9571007
"response": {
9581008
"solver_response": {
959-
"status": 2,
1009+
"status": "FeasibleFound",
9601010
"solution": {
9611011
"problem_category": 1,
9621012
"primal_solution": [0.0, 0.0],

0 commit comments

Comments
 (0)