Skip to content

Implement python and C api for semi-continuous variables#1225

Open
hlinsen wants to merge 1 commit into
NVIDIA:mainfrom
hlinsen:sc-var-api
Open

Implement python and C api for semi-continuous variables#1225
hlinsen wants to merge 1 commit into
NVIDIA:mainfrom
hlinsen:sc-var-api

Conversation

@hlinsen
Copy link
Copy Markdown
Contributor

@hlinsen hlinsen commented May 14, 2026

This PR adds semi continuous to python and C APIs.
Closes: #1156

@hlinsen hlinsen added this to the 26.06 milestone May 14, 2026
@hlinsen hlinsen requested review from a team as code owners May 14, 2026 22:44
@hlinsen hlinsen requested a review from rgsl888prabhu May 14, 2026 22:44
@hlinsen hlinsen added feature request New feature or request non-breaking Introduces a non-breaking change labels May 14, 2026
@hlinsen hlinsen requested review from akifcorduk and chris-maes May 14, 2026 22:44
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds comprehensive support for semi-continuous variables throughout cuOpt. It introduces the CUOPT_SEMI_CONTINUOUS constant, implements centralized variable type conversion utilities, updates the MPS parser, refactors C/C++/CUDA APIs to use these utilities, and extends Python bindings with corresponding type definitions and tests.

Changes

Semi-continuous variable type support

Layer / File(s) Summary
Variable type constants and conversion utilities
cpp/include/cuopt/linear_programming/constants.h, cpp/include/cuopt/linear_programming/cuopt_c.h, cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp
New CUOPT_SEMI_CONTINUOUS constant mapped to 'S', Doxygen documentation expanded across cuOptCreateProblem, cuOptCreateRangedProblem, and cuOptGetVariableTypes, and var_type_to_char() utility added to convert var_t enum to MPS character codes.
MPS parser semi-continuous support
cpp/libmps_parser/src/mps_parser.cpp
Bound-type converter now recognizes only "SC" for semi-continuous, removing previous "LC" handling that will now error.
C API implementation with centralized conversion
cpp/src/pdlp/cuopt_c.cpp
cuOptCreateProblem, cuOptCreateRangedProblem, and cuOptGetVariableTypes refactored to use detail::char_to_var_type() and detail::var_type_to_char() helpers instead of inline conditional logic.
Optimization problem handling and category detection
cpp/src/pdlp/cpu_optimization_problem.cpp, cpp/src/pdlp/optimization_problem.cu
Problem category detection updated to count both INTEGER and SEMI_CONTINUOUS as discrete types; variable type encoding centralized via var_type_to_char() in CPU and GPU implementations.
Semi-continuous presolve constraint bounds
cpp/src/mip_heuristics/presolve/semi_continuous.cu
New ensure_constraint_bounds_populated() helper populates missing constraint lower/upper bound arrays on optimization problems using device-side transforms, ensuring bounds are available before reformulation.
C API test for semi-continuous problem
cpp/tests/linear_programming/c_api_tests/c_api_test.c, cpp/tests/linear_programming/c_api_tests/c_api_tests.h, cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp
New test function constructs a two-variable model (one semi-continuous, one continuous), verifies variable type preservation, confirms MIP classification, solves, and retrieves termination status, objective, and solution values.
Python API semi-continuous support
python/cuopt/cuopt/linear_programming/problem.py
VType enum extended with SEMI_CONTINUOUS = "S", docstrings updated to document the new type, and Problem.IsMIP property broadened to recognize semi-continuous variables as discrete types.
Python solver and test for semi-continuous variables
python/cuopt/cuopt/linear_programming/solver/solver.py, python/cuopt/cuopt/tests/linear_programming/test_python_API.py
Solver's MIP detection enhanced to recognize both string and bytes encodings of semi-continuous type code; new end-to-end test validates creating and solving a semi-continuous variable model.
Documentation for semi-continuous constant
docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst
C API reference updated to document CUOPT_SEMI_CONTINUOUS among the variable type constants.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding semi-continuous variable support to both Python and C APIs.
Description check ✅ Passed The description is related to the changeset, mentioning that the PR adds semi-continuous support to Python and C APIs and references the related issue.
Linked Issues check ✅ Passed All coding requirements from issue #1156 are met: semi-continuous variable support is implemented in Python API (VType enum, SEMI_CONTINUOUS constant, documentation updates, Problem.IsMIP logic) and C API (constants, Doxygen docs, C API test coverage).
Out of Scope Changes check ✅ Passed All changes are directly related to implementing semi-continuous variable support across the codebase: constants, C API tests, Python API, MPS parsing, CPU optimization problem handling, and supporting utility functions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
python/cuopt/cuopt/tests/linear_programming/test_python_API.py (1)

163-183: ⚡ Quick win

Add one test for the non-zero semi-continuous branch.

Line 163 currently validates only the x = 0 branch. Please add a case that forces x into [lb, ub] so both semi-continuous behaviors are covered.

Proposed test addition
+def test_semi_continuous_variable_nonzero_branch():
+    prob = Problem("Semi-continuous nonzero")
+    x = prob.addVariable(lb=5.0, ub=10.0, vtype=SEMI_CONTINUOUS, name="x")
+    prob.addConstraint(x >= 6.0)
+    prob.setObjective(x, sense=MINIMIZE)
+
+    prob.solve()
+
+    assert prob.Status.name == "Optimal"
+    assert prob.IsMIP
+    assert x.Value == pytest.approx(6.0)
+    assert prob.ObjValue == pytest.approx(6.0)

As per coding guidelines, "Flag missing coverage for edge cases (empty, infeasible, unbounded, degenerate) when adding new code paths".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuopt/cuopt/tests/linear_programming/test_python_API.py` around lines
163 - 183, Add a second test that forces the non-zero semi-continuous branch by
constructing a problem where x must lie in [lb, ub] (e.g., create a new Problem
like "Semi-continuous-nonzero", add x = prob.addVariable(lb=5.0, ub=10.0,
vtype=SEMI_CONTINUOUS, name="x") and y = prob.addVariable(lb=0.0, ub=1.0,
vtype=CONTINUOUS, name="y"), impose a constraint that makes x >= lb such as
prob.addConstraint(x + y == 6.0) so y=1 and x=5 is feasible, set the objective
prob.setObjective(x, sense=MINIMIZE), solve with SolverSettings and assert
prob.Status.name == "Optimal", prob.ObjValue == pytest.approx(5.0), x.Value ==
pytest.approx(5.0) and y.Value == pytest.approx(1.0) to cover the non-zero
semi-continuous branch.)
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/mip_heuristics/presolve/semi_continuous.cu`:
- Around line 42-50: The current guard in ensure_constraint_bounds_populated
returns if either get_constraint_lower_bounds() OR get_constraint_upper_bounds()
is non-empty, which leaves the other side potentially empty and breaks later
rebuild logic; change the logic so it only returns when BOTH lower and upper are
populated, and if one side is missing populate the missing bounds to the correct
size (using get_constraint_bounds() when available or sizing from
get_row_types()) so that get_constraint_lower_bounds() and
get_constraint_upper_bounds() are both fully populated before proceeding; update
code in ensure_constraint_bounds_populated to check both sides, and when one is
empty create/fill it with appropriate default or copied values consistent with
existing get_constraint_bounds().

In `@cpp/src/pdlp/cuopt_c.cpp`:
- Around line 198-199: Validate the contents of the input variable_types array
before converting with detail::char_to_var_type: iterate the provided
variable_types and check each code against the supported set, and if any
unknown/unsupported char is found return CUOPT_INVALID_ARGUMENT immediately
(both create paths that populate variable_types_host from variable_types),
instead of coercing or calling detail::char_to_var_type; update the code around
the conversion sites (where variable_types_host[j] =
detail::char_to_var_type(variable_types[j]) is used) to perform the validation
first and only call char_to_var_type after all codes are confirmed valid.

---

Nitpick comments:
In `@python/cuopt/cuopt/tests/linear_programming/test_python_API.py`:
- Around line 163-183: Add a second test that forces the non-zero
semi-continuous branch by constructing a problem where x must lie in [lb, ub]
(e.g., create a new Problem like "Semi-continuous-nonzero", add x =
prob.addVariable(lb=5.0, ub=10.0, vtype=SEMI_CONTINUOUS, name="x") and y =
prob.addVariable(lb=0.0, ub=1.0, vtype=CONTINUOUS, name="y"), impose a
constraint that makes x >= lb such as prob.addConstraint(x + y == 6.0) so y=1
and x=5 is feasible, set the objective prob.setObjective(x, sense=MINIMIZE),
solve with SolverSettings and assert prob.Status.name == "Optimal",
prob.ObjValue == pytest.approx(5.0), x.Value == pytest.approx(5.0) and y.Value
== pytest.approx(1.0) to cover the non-zero semi-continuous branch.)
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e05ea914-f9eb-4929-b144-bb15cbad579f

📥 Commits

Reviewing files that changed from the base of the PR and between 378c601 and e04bf77.

📒 Files selected for processing (15)
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp
  • cpp/libmps_parser/src/mps_parser.cpp
  • cpp/src/mip_heuristics/presolve/semi_continuous.cu
  • cpp/src/pdlp/cpu_optimization_problem.cpp
  • cpp/src/pdlp/cuopt_c.cpp
  • cpp/src/pdlp/optimization_problem.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
  • cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp
  • cpp/tests/linear_programming/c_api_tests/c_api_tests.h
  • docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst
  • python/cuopt/cuopt/linear_programming/problem.py
  • python/cuopt/cuopt/linear_programming/solver/solver.py
  • python/cuopt/cuopt/tests/linear_programming/test_python_API.py

Comment on lines +42 to +50
template <typename i_t, typename f_t>
void ensure_constraint_bounds_populated(optimization_problem_t<i_t, f_t>& op_problem)
{
if (!op_problem.get_constraint_lower_bounds().is_empty() ||
!op_problem.get_constraint_upper_bounds().is_empty()) {
return;
}
if (op_problem.get_row_types().is_empty() || op_problem.get_constraint_bounds().is_empty()) {
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Populate missing bounds when either side is absent (Line 45).

The guard at Line 45 returns when either lower or upper bounds is present. If only one side exists, the other remains empty, but later rebuild logic assumes both are fully populated and can mis-size bound arrays.

Proposed fix
 template <typename i_t, typename f_t>
 void ensure_constraint_bounds_populated(optimization_problem_t<i_t, f_t>& op_problem)
 {
-  if (!op_problem.get_constraint_lower_bounds().is_empty() ||
-      !op_problem.get_constraint_upper_bounds().is_empty()) {
+  if (!op_problem.get_constraint_lower_bounds().is_empty() &&
+      !op_problem.get_constraint_upper_bounds().is_empty()) {
     return;
   }
   if (op_problem.get_row_types().is_empty() || op_problem.get_constraint_bounds().is_empty()) {
     return;
   }

As per coding guidelines, "Flag missing input validation at library and server boundaries".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/mip_heuristics/presolve/semi_continuous.cu` around lines 42 - 50, The
current guard in ensure_constraint_bounds_populated returns if either
get_constraint_lower_bounds() OR get_constraint_upper_bounds() is non-empty,
which leaves the other side potentially empty and breaks later rebuild logic;
change the logic so it only returns when BOTH lower and upper are populated, and
if one side is missing populate the missing bounds to the correct size (using
get_constraint_bounds() when available or sizing from get_row_types()) so that
get_constraint_lower_bounds() and get_constraint_upper_bounds() are both fully
populated before proceeding; update code in ensure_constraint_bounds_populated
to check both sides, and when one is empty create/fill it with appropriate
default or copied values consistent with existing get_constraint_bounds().

Comment thread cpp/src/pdlp/cuopt_c.cpp
Comment on lines +198 to 199
variable_types_host[j] = detail::char_to_var_type(variable_types[j]);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate variable_types codes before conversion.

Unsupported type chars are currently coerced instead of rejected, which silently mutates caller input into a different model. Please return CUOPT_INVALID_ARGUMENT for unknown codes in both create paths before calling char_to_var_type(...).

Suggested patch pattern
-    for (int j = 0; j < num_variables; j++) {
-      variable_types_host[j] = detail::char_to_var_type(variable_types[j]);
-    }
+    for (int j = 0; j < num_variables; j++) {
+      const char vtype = variable_types[j];
+      if (vtype != CUOPT_CONTINUOUS && vtype != CUOPT_INTEGER &&
+          vtype != CUOPT_SEMI_CONTINUOUS && vtype != 'B') {
+        delete problem_and_stream;
+        return CUOPT_INVALID_ARGUMENT;
+      }
+      variable_types_host[j] = detail::char_to_var_type(vtype);
+    }

Also applies to: 259-260

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/pdlp/cuopt_c.cpp` around lines 198 - 199, Validate the contents of
the input variable_types array before converting with detail::char_to_var_type:
iterate the provided variable_types and check each code against the supported
set, and if any unknown/unsupported char is found return CUOPT_INVALID_ARGUMENT
immediately (both create paths that populate variable_types_host from
variable_types), instead of coercing or calling detail::char_to_var_type; update
the code around the conversion sites (where variable_types_host[j] =
detail::char_to_var_type(variable_types[j]) is used) to perform the validation
first and only call char_to_var_type after all codes are confirmed valid.

} else if (str == "UI") {
return UpperBoundIntegerVariable;
} else if (str == "SC" || str == "LC") {
} else if (str == "SC") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change to mps_parser intentional?

Copy link
Copy Markdown
Contributor Author

@hlinsen hlinsen May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The semi continuous variables is always defined with the upper bound SC which is mandatory. LC shouldn't have been added.

Copy link
Copy Markdown
Collaborator

@rgsl888prabhu rgsl888prabhu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rest looks good, I would suggest adding an example to docs under python. Not a blocking comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA] Add python support for semi-continuous variables

3 participants