From e04bf773b6d81e2138cd6c3eb5e09341fcb6754c Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 14 May 2026 12:35:29 -0700 Subject: [PATCH 1/3] Implement python and C api for semi-continuous variables --- .../cuopt/linear_programming/constants.h | 5 +- .../cuopt/linear_programming/cuopt_c.h | 9 +- .../optimization_problem_utils.hpp | 7 ++ cpp/libmps_parser/src/mps_parser.cpp | 2 +- .../presolve/semi_continuous.cu | 31 +++++ cpp/src/pdlp/cpu_optimization_problem.cpp | 14 ++- cpp/src/pdlp/cuopt_c.cpp | 9 +- cpp/src/pdlp/optimization_problem.cu | 3 +- .../c_api_tests/c_api_test.c | 111 ++++++++++++++++++ .../c_api_tests/c_api_tests.cpp | 13 ++ .../c_api_tests/c_api_tests.h | 3 + .../cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst | 1 + .../cuopt/cuopt/linear_programming/problem.py | 14 ++- .../cuopt/linear_programming/solver/solver.py | 11 +- .../linear_programming/test_python_API.py | 23 ++++ 15 files changed, 228 insertions(+), 28 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index b251b3eaba..8dcc8b06c3 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -134,8 +134,9 @@ #define CUOPT_EQUAL 'E' /* @brief The variable type constants */ -#define CUOPT_CONTINUOUS 'C' -#define CUOPT_INTEGER 'I' +#define CUOPT_CONTINUOUS 'C' +#define CUOPT_INTEGER 'I' +#define CUOPT_SEMI_CONTINUOUS 'S' /* @brief The infinity constant */ #ifdef __cplusplus diff --git a/cpp/include/cuopt/linear_programming/cuopt_c.h b/cpp/include/cuopt/linear_programming/cuopt_c.h index 4c4d44c764..462365c777 100644 --- a/cpp/include/cuopt/linear_programming/cuopt_c.h +++ b/cpp/include/cuopt/linear_programming/cuopt_c.h @@ -164,7 +164,8 @@ cuopt_int_t cuOptWriteProblem(cuOptOptimizationProblem problem, * @param[in] upper_bounds A pointer to an array of type cuopt_float_t of size num_variables * containing the upper bounds of the variables * @param[in] variable_types A pointer to an array of type char of size num_variables - * containing the types of the variables (CUOPT_CONTINUOUS or CUOPT_INTEGER) + * containing the types of the variables (CUOPT_CONTINUOUS, CUOPT_INTEGER, or + * CUOPT_SEMI_CONTINUOUS) * @param[out] problem_ptr Pointer to store the created optimization problem * @return CUOPT_SUCCESS if successful, CUOPT_ERROR otherwise */ @@ -229,8 +230,8 @@ cuopt_int_t cuOptCreateProblem(cuopt_int_t num_constraints, * cuopt_float_t of size num_variables containing the upper bounds of the variables. * * @param[in] variable_types - A pointer to an array of type char of size - * num_variables containing the types of the variables (CUOPT_CONTINUOUS or - * CUOPT_INTEGER). + * num_variables containing the types of the variables (CUOPT_CONTINUOUS, + * CUOPT_INTEGER, or CUOPT_SEMI_CONTINUOUS). * * @param[out] problem_ptr - A pointer to a cuOptOptimizationProblem. * On output the problem will be created and initialized with the provided data. @@ -585,7 +586,7 @@ cuopt_int_t cuOptGetVariableUpperBounds(cuOptOptimizationProblem problem, * * @param[out] variable_types_ptr - A pointer to an array of type char of size * num_variables that on output will contain the types of the variables - * (CUOPT_CONTINUOUS or CUOPT_INTEGER). + * (CUOPT_CONTINUOUS, CUOPT_INTEGER, or CUOPT_SEMI_CONTINUOUS). * * @return A status code indicating success or failure. */ diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp index 1adffb1603..a1c0a039b0 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp @@ -25,6 +25,13 @@ inline constexpr var_t char_to_var_type(char variable_type) return var_t::CONTINUOUS; } +inline constexpr char var_type_to_char(var_t variable_type) +{ + if (variable_type == var_t::INTEGER) { return 'I'; } + if (variable_type == var_t::SEMI_CONTINUOUS) { return 'S'; } + return 'C'; +} + } // namespace detail /** diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index c58a843ed5..2b72b8b038 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -250,7 +250,7 @@ BoundType convert(std::string_view str) return LowerBoundIntegerVariable; } else if (str == "UI") { return UpperBoundIntegerVariable; - } else if (str == "SC" || str == "LC") { + } else if (str == "SC") { return SemiContinuousVariable; } else { mps_parser_expects(false, diff --git a/cpp/src/mip_heuristics/presolve/semi_continuous.cu b/cpp/src/mip_heuristics/presolve/semi_continuous.cu index 15728d02bb..1fa72b4c49 100644 --- a/cpp/src/mip_heuristics/presolve/semi_continuous.cu +++ b/cpp/src/mip_heuristics/presolve/semi_continuous.cu @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,32 @@ bool is_effectively_infinite_sc_upper_bound(f_t ub) return !std::isfinite(ub) || ub >= static_cast(sc_infinity_threshold); } +template +void ensure_constraint_bounds_populated(optimization_problem_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; + } + const auto* handle_ptr = op_problem.get_handle_ptr(); + const auto stream = handle_ptr->get_stream(); + const i_t n = static_cast(op_problem.get_row_types().size()); + rmm::device_uvector clb(n, stream); + rmm::device_uvector cub(n, stream); + auto in_first = thrust::make_zip_iterator(thrust::make_tuple( + op_problem.get_row_types().cbegin(), op_problem.get_constraint_bounds().cbegin())); + auto in_last = thrust::make_zip_iterator(thrust::make_tuple( + op_problem.get_row_types().cend(), op_problem.get_constraint_bounds().cend())); + auto out_first = thrust::make_zip_iterator(thrust::make_tuple(clb.begin(), cub.begin())); + thrust::transform( + handle_ptr->get_thrust_policy(), in_first, in_last, out_first, transform_bounds_functor{}); + op_problem.set_constraint_lower_bounds(clb.data(), n); + op_problem.set_constraint_upper_bounds(cub.data(), n); +} + template std::vector call_host_bounds_strengthening(const optimization_problem_t& op_problem, const mip_solver_settings_t& settings, @@ -147,6 +174,8 @@ bool reformulate_semi_continuous(optimization_problem_t& op_problem, op_relaxed.set_variable_types(relaxed_types.data(), n_orig); op_relaxed.set_variable_lower_bounds(relaxed_lb.data(), n_orig); op_relaxed.set_variable_upper_bounds(relaxed_ub.data(), n_orig); + + ensure_constraint_bounds_populated(op_relaxed); } // 3. Run deterministic CPU bounds strengthening on the relaxed problem to tighten UBs. @@ -159,6 +188,8 @@ bool reformulate_semi_continuous(optimization_problem_t& op_problem, // 4. Fetch all host arrays we need to extend with the new binary variables // and linking constraints. + ensure_constraint_bounds_populated(op_problem); + auto obj_c = op_problem.get_objective_coefficients_host(); auto A_vals = op_problem.get_constraint_matrix_values_host(); auto A_idx = op_problem.get_constraint_matrix_indices_host(); diff --git a/cpp/src/pdlp/cpu_optimization_problem.cpp b/cpp/src/pdlp/cpu_optimization_problem.cpp index de1f74ed47..5d354fa42d 100644 --- a/cpp/src/pdlp/cpu_optimization_problem.cpp +++ b/cpp/src/pdlp/cpu_optimization_problem.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -176,13 +177,16 @@ void cpu_optimization_problem_t::set_variable_types(const var_t* varia std::copy(variable_types, variable_types + size, variable_types_.begin()); // Auto-detect problem category based on variable types (matching original optimization_problem_t) - i_t n_integer = std::count_if( - variable_types_.begin(), variable_types_.end(), [](auto val) { return val == var_t::INTEGER; }); + i_t n_discrete = std::count_if(variable_types_.begin(), variable_types_.end(), [](auto val) { + return val == var_t::INTEGER || val == var_t::SEMI_CONTINUOUS; + }); // By default it is LP - if (n_integer == size) { + if (n_discrete == size) { problem_category_ = problem_category_t::IP; - } else if (n_integer > 0) { + } else if (n_discrete > 0) { problem_category_ = problem_category_t::MIP; + } else { + problem_category_ = problem_category_t::LP; } } @@ -749,7 +753,7 @@ void cpu_optimization_problem_t::write_to_mps(const std::string& mps_f var_types_char.resize(variable_types_.size()); for (size_t i = 0; i < var_types_char.size(); ++i) { - var_types_char[i] = (variable_types_[i] == var_t::INTEGER) ? 'I' : 'C'; + var_types_char[i] = detail::var_type_to_char(variable_types_[i]); } data_model_view.set_variable_types(var_types_char.data(), var_types_char.size()); diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index ed2eab02f2..bf462fe9e5 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -195,8 +195,7 @@ cuopt_int_t cuOptCreateProblem(cuopt_int_t num_constraints, // Set variable types (problem category is auto-detected) std::vector variable_types_host(num_variables); for (int j = 0; j < num_variables; j++) { - variable_types_host[j] = - variable_types[j] == CUOPT_CONTINUOUS ? var_t::CONTINUOUS : var_t::INTEGER; + variable_types_host[j] = detail::char_to_var_type(variable_types[j]); } problem->set_variable_types(variable_types_host.data(), num_variables); @@ -257,8 +256,7 @@ cuopt_int_t cuOptCreateRangedProblem(cuopt_int_t num_constraints, std::vector variable_types_host(num_variables); if (variable_types != nullptr) { for (int j = 0; j < num_variables; j++) { - variable_types_host[j] = - variable_types[j] == CUOPT_CONTINUOUS ? var_t::CONTINUOUS : var_t::INTEGER; + variable_types_host[j] = detail::char_to_var_type(variable_types[j]); } } else { // Default to all continuous @@ -614,8 +612,7 @@ cuopt_int_t cuOptGetVariableTypes(cuOptOptimizationProblem problem, char* variab // Convert var_t enum to C API char values for (size_t j = 0; j < variable_types_host.size(); j++) { - variable_types_ptr[j] = - variable_types_host[j] == var_t::INTEGER ? CUOPT_INTEGER : CUOPT_CONTINUOUS; + variable_types_ptr[j] = detail::var_type_to_char(variable_types_host[j]); } return CUOPT_SUCCESS; } diff --git a/cpp/src/pdlp/optimization_problem.cu b/cpp/src/pdlp/optimization_problem.cu index a6f0d30ea8..f7fe351e80 100644 --- a/cpp/src/pdlp/optimization_problem.cu +++ b/cpp/src/pdlp/optimization_problem.cu @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -847,7 +848,7 @@ void optimization_problem_t::write_to_mps(const std::string& mps_file_ // Convert enum types to char types for (size_t i = 0; i < variable_types.size(); ++i) { - variable_types[i] = (enum_variable_types[i] == var_t::INTEGER) ? 'I' : 'C'; + variable_types[i] = detail::var_type_to_char(enum_variable_types[i]); } data_model_view.set_variable_types(variable_types.data(), variable_types.size()); diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_test.c b/cpp/tests/linear_programming/c_api_tests/c_api_test.c index 639aa8c379..5e1d3122dc 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_test.c +++ b/cpp/tests/linear_programming/c_api_tests/c_api_test.c @@ -1041,6 +1041,117 @@ cuopt_int_t test_ranged_problem(cuopt_int_t* termination_status_ptr, cuopt_float return status; } +cuopt_int_t test_semi_continuous_problem(cuopt_int_t* termination_status_ptr, + cuopt_float_t* objective_ptr, + cuopt_float_t* solution_values) +{ + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + + /* Minimize x with x semi-continuous and x + y = 1. + Since x is either 0 or in [5, 10], the optimum is x = 0, y = 1. + If CUOPT_SEMI_CONTINUOUS is treated as integer, this model is infeasible. */ + cuopt_int_t num_variables = 2; + cuopt_int_t num_constraints = 1; + cuopt_int_t row_offsets[] = {0, 2}; + cuopt_int_t column_indices[] = {0, 1}; + cuopt_float_t values[] = {1.0, 1.0}; + char constraint_sense[] = {CUOPT_EQUAL}; + cuopt_float_t rhs[] = {1.0}; + cuopt_float_t lower_bounds[] = {5.0, 0.0}; + cuopt_float_t upper_bounds[] = {10.0, 1.0}; + char variable_types[] = {CUOPT_SEMI_CONTINUOUS, CUOPT_CONTINUOUS}; + cuopt_float_t objective_coefficients[] = {1.0, 0.0}; + char check_variable_types[2]; + cuopt_int_t is_mip; + cuopt_int_t status; + + status = cuOptCreateProblem(num_constraints, + num_variables, + CUOPT_MINIMIZE, + 0.0, + objective_coefficients, + row_offsets, + column_indices, + values, + constraint_sense, + rhs, + lower_bounds, + upper_bounds, + variable_types, + &problem); + if (status != CUOPT_SUCCESS) { + printf("Error creating semi-continuous problem\n"); + goto DONE; + } + + status = cuOptGetVariableTypes(problem, check_variable_types); + if (status != CUOPT_SUCCESS) { + printf("Error getting variable types for semi-continuous problem\n"); + goto DONE; + } + if (check_variable_types[0] != CUOPT_SEMI_CONTINUOUS || + check_variable_types[1] != CUOPT_CONTINUOUS) { + printf("Error: semi-continuous variable types were not preserved\n"); + status = -1; + goto DONE; + } + + status = cuOptIsMIP(problem, &is_mip); + if (status != CUOPT_SUCCESS) { + printf("Error getting MIP flag for semi-continuous problem\n"); + goto DONE; + } + if (!is_mip) { + printf("Error: semi-continuous problem was not detected as a MIP\n"); + status = -1; + goto DONE; + } + + status = cuOptCreateSolverSettings(&settings); + if (status != CUOPT_SUCCESS) { + printf("Error creating solver settings\n"); + goto DONE; + } + status = cuOptSetFloatParameter(settings, CUOPT_TIME_LIMIT, 10.0); + if (status != CUOPT_SUCCESS) { + printf("Error setting time limit\n"); + goto DONE; + } + + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) { + printf("Error solving semi-continuous problem\n"); + goto DONE; + } + + status = cuOptGetTerminationStatus(solution, termination_status_ptr); + if (status != CUOPT_SUCCESS) { + printf("Error getting termination status\n"); + goto DONE; + } + + status = cuOptGetObjectiveValue(solution, objective_ptr); + if (status != CUOPT_SUCCESS) { + printf("Error getting objective value\n"); + goto DONE; + } + + status = cuOptGetPrimalSolution(solution, solution_values); + if (status != CUOPT_SUCCESS) { + printf("Error getting primal solution\n"); + goto DONE; + } + +DONE: + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + + return status; +} + // Test invalid bounds scenario (what MOI wrapper was producing) cuopt_int_t test_invalid_bounds(cuopt_int_t test_mip) { diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp index 1912b15cb5..3f24e3a0cd 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp @@ -125,6 +125,19 @@ TEST(c_api, test_ranged_problem) EXPECT_NEAR(objective, 32.0, 1e-3); } +TEST(c_api, test_semi_continuous_problem) +{ + cuopt_int_t termination_status = CUOPT_TERMINATION_STATUS_NO_TERMINATION; + cuopt_float_t objective = 0.0; + cuopt_float_t solution_values[2] = {0.0, 0.0}; + ASSERT_EQ(test_semi_continuous_problem(&termination_status, &objective, solution_values), + CUOPT_SUCCESS); + EXPECT_EQ(termination_status, CUOPT_TERMINATION_STATUS_OPTIMAL); + EXPECT_NEAR(objective, 0.0, 1e-6); + EXPECT_NEAR(solution_values[0], 0.0, 1e-6); + EXPECT_NEAR(solution_values[1], 1.0, 1e-6); +} + TEST(c_api, test_invalid_bounds) { // Test LP codepath diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.h b/cpp/tests/linear_programming/c_api_tests/c_api_tests.h index 402c7d06a5..7720cb2c0d 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.h +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.h @@ -31,6 +31,9 @@ cuopt_int_t test_bad_parameter_name(); cuopt_int_t test_mip_get_callbacks_only(); cuopt_int_t test_mip_get_set_callbacks(); cuopt_int_t test_ranged_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr); +cuopt_int_t test_semi_continuous_problem(cuopt_int_t* termination_status_ptr, + cuopt_float_t* objective_ptr, + cuopt_float_t* solution_values); cuopt_int_t test_invalid_bounds(cuopt_int_t test_mip); cuopt_int_t test_quadratic_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr); diff --git a/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst b/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst index e8511cfde0..5321a2247e 100644 --- a/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst +++ b/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst @@ -81,6 +81,7 @@ These constants are used to define the the variable type in the :c:func:`cuOptCr .. doxygendefine:: CUOPT_CONTINUOUS .. doxygendefine:: CUOPT_INTEGER +.. doxygendefine:: CUOPT_SEMI_CONTINUOUS Infinity Constant ----------------- diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 62164f365f..53fe3317b3 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -17,18 +17,21 @@ class VType(str, Enum): """ - The type of a variable is either continuous or integer. + The type of a variable is continuous, integer, or semi-continuous. Variable Types can be directly used as a constant. CONTINUOUS is VType.CONTINUOUS INTEGER is VType.INTEGER + SEMI_CONTINUOUS is VType.SEMI_CONTINUOUS """ CONTINUOUS = "C" INTEGER = "I" + SEMI_CONTINUOUS = "S" CONTINUOUS = VType.CONTINUOUS INTEGER = VType.INTEGER +SEMI_CONTINUOUS = VType.SEMI_CONTINUOUS class CType(str, Enum): @@ -90,7 +93,7 @@ class Variable: ---------- VariableName : str Name of the Variable. - VariableType : CONTINUOUS or INTEGER + VariableType : CONTINUOUS, INTEGER, or SEMI_CONTINUOUS Variable type. LB : float Lower Bound of the Variable. @@ -173,7 +176,7 @@ def getUpperBound(self): def setVariableType(self, val): """ Sets the variable type of the variable. - Variable types can be either CONTINUOUS or INTEGER. + Variable types can be CONTINUOUS, INTEGER, or SEMI_CONTINUOUS. """ self.VariableType = val @@ -1500,7 +1503,8 @@ def addVariable( ub : float Upper bound of the variable. Defaults to infinity. vtype : enum :py:class:`VType` - vtype.CONTINUOUS or vtype.INTEGER. Defaults to CONTINUOUS. + vtype.CONTINUOUS, vtype.INTEGER, or vtype.SEMI_CONTINUOUS. + Defaults to CONTINUOUS. name : string Name of the variable. Optional. @@ -1835,7 +1839,7 @@ def NumNZs(self): def IsMIP(self): # Returns if the problem is a MIP problem. for var in self.vars: - if var.VariableType == "I": + if var.VariableType in ("I", "S", b"I", b"S"): return True return False diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index e80ad3b6f4..b21e52d3c2 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import os @@ -88,12 +88,15 @@ def is_mip(var_types): if len(set(map(type, var_types))) == 1: # Homogeneous - use appropriate check if isinstance(var_types[0], bytes): - return b"I" in var_types + return b"I" in var_types or b"S" in var_types else: - return "I" in var_types + return "I" in var_types or "S" in var_types else: # Mixed types - fallback to comprehensive check - return any(vt == "I" or vt == b"I" for vt in var_types) + return any( + vt == "I" or vt == b"I" or vt == "S" or vt == b"S" + for vt in var_types + ) s = solver_wrapper.Solve( data_model, diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 467d714fee..42ee2519ca 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -16,6 +16,7 @@ INTEGER, MAXIMIZE, MINIMIZE, + SEMI_CONTINUOUS, CType, Problem, VType, @@ -159,6 +160,28 @@ def test_model(): assert prob.ObjValue == pytest.approx(5 * x.Value + 3 * y.Value + 70) +def test_semi_continuous_variable(): + prob = Problem("Semi-continuous") + x = prob.addVariable(lb=5.0, ub=10.0, vtype=SEMI_CONTINUOUS, name="x") + y = prob.addVariable(lb=0.0, ub=1.0, vtype=CONTINUOUS, name="y") + + prob.addConstraint(x + y == 1.0) + prob.setObjective(x, sense=MINIMIZE) + + assert x.getVariableType() == VType.SEMI_CONTINUOUS + assert prob.IsMIP + + settings = SolverSettings() + settings.set_parameter("time_limit", 10) + + prob.solve(settings) + + assert prob.Status.name == "Optimal" + assert prob.ObjValue == pytest.approx(0.0) + assert x.Value == pytest.approx(0.0) + assert y.Value == pytest.approx(1.0) + + def test_linear_expression(): prob = Problem() From 17b9147d253a947d9dcdbfa44533c0b31e44ab73 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 18 May 2026 23:12:25 -0700 Subject: [PATCH 2/3] Check variable type --- .../optimization_problem_utils.hpp | 5 +++++ cpp/src/pdlp/cuopt_c.cpp | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp index 9a3c039a60..53ef6da4e5 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp @@ -18,6 +18,11 @@ namespace cuopt::linear_programming { namespace detail { +inline constexpr bool is_valid_public_var_type_code(char variable_type) +{ + return variable_type == 'C' || variable_type == 'I' || variable_type == 'S'; +} + inline constexpr var_t char_to_var_type(char variable_type) { if (variable_type == 'I' || variable_type == 'B') { return var_t::INTEGER; } diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index dc6b4e5458..549a287c13 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -172,6 +172,11 @@ cuopt_int_t cuOptCreateProblem(cuopt_int_t num_constraints, variable_types == nullptr) { return CUOPT_INVALID_ARGUMENT; } + for (int j = 0; j < num_variables; j++) { + if (!detail::is_valid_public_var_type_code(variable_types[j])) { + return CUOPT_INVALID_ARGUMENT; + } + } problem_and_stream_view_t* problem_and_stream = new problem_and_stream_view_t(get_memory_backend_type()); @@ -231,6 +236,13 @@ cuopt_int_t cuOptCreateRangedProblem(cuopt_int_t num_constraints, variable_upper_bounds == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (variable_types != nullptr) { + for (int j = 0; j < num_variables; j++) { + if (!detail::is_valid_public_var_type_code(variable_types[j])) { + return CUOPT_INVALID_ARGUMENT; + } + } + } problem_and_stream_view_t* problem_and_stream = new problem_and_stream_view_t(get_memory_backend_type()); From 33107e0f25bd4349188d7ae0ac21648f28c18259 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 18 May 2026 23:15:34 -0700 Subject: [PATCH 3/3] Add example to python doc --- .../lp-qp-milp/lp-qp-milp-examples.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst index 2edd625fbc..8237a560df 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst @@ -68,6 +68,25 @@ The response is as follows: Objective value = 303.0 +Semi-continuous Variable Example +-------------------------------- + +:download:`semi_continuous_example.py ` + +.. literalinclude:: examples/semi_continuous_example.py + :language: python + :linenos: + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.00 seconds + x = 0.0 + y = 1.0 + Objective value = 0.0 + + Advanced Example: Production Planning -------------------------------------