diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 09c8bec64..4e5c1af5a 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -784,9 +784,12 @@ void mps_parser_t::parse_string(char* buf) variable_lower_bounds[i] = 0; variable_upper_bounds[i] = 1; } - mps_parser_expects(variable_lower_bounds[i] <= variable_upper_bounds[i], - error_type_t::ValidationError, - "MPS Parser Internal Error - Please contact cuOpt team"); + if (variable_lower_bounds[i] > variable_upper_bounds[i]) { + printf("WARNING: Variable %d has crossing bounds: %f > %f\n", + i, + variable_lower_bounds[i], + variable_upper_bounds[i]); + } } } diff --git a/cpp/src/linear_programming/solve.cu b/cpp/src/linear_programming/solve.cu index 156dd5296..ef7528751 100644 --- a/cpp/src/linear_programming/solve.cu +++ b/cpp/src/linear_programming/solve.cu @@ -590,6 +590,12 @@ optimization_problem_solution_t solve_lp(optimization_problem_t::check_initial_solution_representation(op_problem, settings); } + // Check for crossing bounds. Return infeasible if there are any + if (problem_checking_t::has_crossing_bounds(op_problem)) { + return optimization_problem_solution_t(pdlp_termination_status_t::PrimalInfeasible, + op_problem.get_handle_ptr()->get_stream()); + } + auto lp_timer = cuopt::timer_t(settings.time_limit); detail::problem_t problem(op_problem); diff --git a/cpp/src/linear_programming/utilities/problem_checking.cu b/cpp/src/linear_programming/utilities/problem_checking.cu index b53021a95..fbbdd1ad9 100644 --- a/cpp/src/linear_programming/utilities/problem_checking.cu +++ b/cpp/src/linear_programming/utilities/problem_checking.cu @@ -22,6 +22,8 @@ #include #include +#include + #include #include #include @@ -320,6 +322,34 @@ void problem_checking_t::check_unscaled_solution( } } +template +bool problem_checking_t::has_crossing_bounds( + const optimization_problem_t& op_problem) +{ + // Check if all variable bounds are valid (upper >= lower) + bool all_variable_bounds_valid = thrust::all_of( + op_problem.get_handle_ptr()->get_thrust_policy(), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(0) + op_problem.get_variable_upper_bounds().size(), + [upper_bounds = make_span(op_problem.get_variable_upper_bounds()), + lower_bounds = make_span(op_problem.get_variable_lower_bounds())] __device__(size_t i) { + return upper_bounds[i] >= lower_bounds[i]; + }); + + // Check if all constraint bounds are valid (upper >= lower) + bool all_constraint_bounds_valid = thrust::all_of( + op_problem.get_handle_ptr()->get_thrust_policy(), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(0) + op_problem.get_constraint_upper_bounds().size(), + [upper_bounds = make_span(op_problem.get_constraint_upper_bounds()), + lower_bounds = make_span(op_problem.get_constraint_lower_bounds())] __device__(size_t i) { + return upper_bounds[i] >= lower_bounds[i]; + }); + + // Return true if any bounds are invalid (crossing) + return !all_variable_bounds_valid || !all_constraint_bounds_valid; +} + #define INSTANTIATE(F_TYPE) template class problem_checking_t; #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/linear_programming/utilities/problem_checking.cuh b/cpp/src/linear_programming/utilities/problem_checking.cuh index 2df0a517a..aeeb5a115 100644 --- a/cpp/src/linear_programming/utilities/problem_checking.cuh +++ b/cpp/src/linear_programming/utilities/problem_checking.cuh @@ -20,16 +20,25 @@ #include #include -#include +namespace rmm { +template +class device_uvector; +} // namespace rmm namespace cuopt::linear_programming { +namespace detail { +template +class problem_t; +} // namespace detail + template class problem_checking_t { public: static void check_csr_representation(const optimization_problem_t& op_problem); // Check all fields and convert row_types to constraints lower/upper bounds if needed static void check_problem_representation(const optimization_problem_t& op_problem); + static bool has_crossing_bounds(const optimization_problem_t& op_problem); static void check_scaled_problem(detail::problem_t const& scaled_problem, detail::problem_t const& op_problem); diff --git a/cpp/src/mip/presolve/third_party_presolve.cpp b/cpp/src/mip/presolve/third_party_presolve.cpp index 211db4605..bc6ebdffa 100644 --- a/cpp/src/mip/presolve/third_party_presolve.cpp +++ b/cpp/src/mip/presolve/third_party_presolve.cpp @@ -30,8 +30,7 @@ namespace cuopt::linear_programming::detail { static papilo::PostsolveStorage post_solve_storage_; -static int presolve_calls_ = 0; -static bool maximize_ = false; +static bool maximize_ = false; template papilo::Problem build_papilo_problem(const optimization_problem_t& op_problem) @@ -356,10 +355,6 @@ std::pair, bool> third_party_presolve_t papilo_problem = build_papilo_problem(op_problem); CUOPT_LOG_INFO("Unpresolved problem:: %d constraints, %d variables, %d nonzeros", @@ -379,7 +374,6 @@ std::pair, bool> third_party_presolve_t(op_problem.get_handle_ptr()), false); } post_solve_storage_ = result.postsolve; @@ -404,9 +398,6 @@ void third_party_presolve_t::undo(rmm::device_uvector& primal_sol bool status_to_skip, rmm::cuda_stream_view stream_view) { - --presolve_calls_; - cuopt_expects( - presolve_calls_ == 0, error_type_t::ValidationError, "Postsolve can only be called once"); if (status_to_skip) { return; } std::vector primal_sol_vec_h(primal_solution.size()); raft::copy(primal_sol_vec_h.data(), primal_solution.data(), primal_solution.size(), stream_view); diff --git a/cpp/src/mip/solve.cu b/cpp/src/mip/solve.cu index edff4a693..7941f41dc 100644 --- a/cpp/src/mip/solve.cu +++ b/cpp/src/mip/solve.cu @@ -179,6 +179,13 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, problem_checking_t::check_problem_representation(op_problem); problem_checking_t::check_initial_solution_representation(op_problem, settings); + // Check for crossing bounds. Return infeasible if there are any + if (problem_checking_t::has_crossing_bounds(op_problem)) { + return mip_solution_t(mip_termination_status_t::Infeasible, + solver_stats_t{}, + op_problem.get_handle_ptr()->get_stream()); + } + auto timer = cuopt::timer_t(time_limit); double presolve_time = 0.0; 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 28472b201..9f841c330 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 @@ -872,3 +872,164 @@ cuopt_int_t test_ranged_problem(cuopt_int_t *termination_status_ptr, cuopt_float return status; } + +// Test invalid bounds scenario (what MOI wrapper was producing) +cuopt_int_t test_invalid_bounds(cuopt_int_t test_mip) +{ + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + + /* Test the invalid bounds scenario: + maximize 2*x + subject to: + x >= 0.2 + x <= 0.5 + x is binary (0 or 1) + + After MOI wrapper processing: + - Lower bound = ceil(max(0.0, 0.2)) = 1.0 + - Upper bound = floor(min(1.0, 0.5)) = 0.0 + - Result: 1.0 <= x <= 0.0 (INVALID!) + */ + + cuopt_int_t num_variables = 1; + cuopt_int_t num_constraints = 2; + cuopt_int_t nnz = 2; + + // CSR format constraint matrix + // From the constraints: + // x >= 0.2 + // x <= 0.5 + cuopt_int_t row_offsets[] = {0, 1, 2}; + cuopt_int_t column_indices[] = {0, 0}; + cuopt_float_t values[] = {1.0, 1.0}; + + // Objective coefficients + // From the objective function: maximize 2*x + cuopt_float_t objective_coefficients[] = {2.0}; + + // Constraint bounds + // From the constraints: + // x >= 0.2 + // x <= 0.5 + cuopt_float_t constraint_upper_bounds[] = {CUOPT_INFINITY, 0.5}; + cuopt_float_t constraint_lower_bounds[] = {0.2, -CUOPT_INFINITY}; + + // Variable bounds - INVALID: lower > upper + // After MOI wrapper processing: + cuopt_float_t var_lower_bounds[] = {1.0}; // ceil(max(0.0, 0.2)) = 1.0 + cuopt_float_t var_upper_bounds[] = {0.0}; // floor(min(1.0, 0.5)) = 0.0 + + // Variable types (binary) + char variable_types[] = {CUOPT_INTEGER}; // Binary variable + if (!test_mip) variable_types[0] = CUOPT_CONTINUOUS; + + cuopt_int_t status; + cuopt_float_t time; + cuopt_int_t termination_status; + cuopt_float_t objective_value; + + printf("Testing invalid bounds scenario (MOI wrapper issue)...\n"); + printf("Problem: Binary variable with bounds 1.0 <= x <= 0.0 (INVALID!)\n"); + + // Create the problem + status = cuOptCreateRangedProblem(num_constraints, + num_variables, + CUOPT_MAXIMIZE, // maximize + 0.0, // objective offset + objective_coefficients, + row_offsets, + column_indices, + values, + constraint_lower_bounds, + constraint_upper_bounds, + var_lower_bounds, + var_upper_bounds, + variable_types, + &problem); + + printf("cuOptCreateRangedProblem returned: %d\n", status); + + if (status != CUOPT_SUCCESS) { + printf("✗ Unexpected error: %d\n", status); + goto DONE; + } + + // If we get here, the problem was created successfully + printf("✓ Problem created successfully\n"); + + // Create solver settings + status = cuOptCreateSolverSettings(&settings); + if (status != CUOPT_SUCCESS) { + printf("Error creating solver settings: %d\n", status); + goto DONE; + } + + // Solve the problem + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) { + printf("Error solving problem: %d\n", status); + goto DONE; + } + + // Get solution information + status = cuOptGetSolveTime(solution, &time); + if (status != CUOPT_SUCCESS) { + printf("Error getting solve time: %d\n", status); + goto DONE; + } + + status = cuOptGetTerminationStatus(solution, &termination_status); + if (status != CUOPT_SUCCESS) { + printf("Error getting termination status: %d\n", status); + goto DONE; + } + if (termination_status != CUOPT_TERIMINATION_STATUS_INFEASIBLE) { + printf("Error: expected termination status to be %d, but got %d\n", + CUOPT_TERIMINATION_STATUS_INFEASIBLE, + termination_status); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + else { + printf("✓ Problem found infeasible as expected\n"); + status = CUOPT_SUCCESS; + goto DONE; + } + + status = cuOptGetObjectiveValue(solution, &objective_value); + if (status != CUOPT_SUCCESS) { + printf("Error getting objective value: %d\n", status); + goto DONE; + } + + // Print results + printf("\nResults:\n"); + printf("--------\n"); + printf("Termination status: %s (%d)\n", termination_status_to_string(termination_status), termination_status); + printf("Solve time: %f seconds\n", time); + printf("Objective value: %f\n", objective_value); + + // Get and print solution variables + cuopt_float_t* solution_values = (cuopt_float_t*)malloc(num_variables * sizeof(cuopt_float_t)); + status = cuOptGetPrimalSolution(solution, solution_values); + if (status != CUOPT_SUCCESS) { + printf("Error getting solution values: %d\n", status); + free(solution_values); + goto DONE; + } + + printf("\nSolution: \n"); + for (cuopt_int_t i = 0; i < num_variables; i++) { + printf("x%d = %f\n", i + 1, solution_values[i]); + } + free(solution_values); + +DONE: + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + + return status; +} 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 d01f4319b..5ccc21a9e 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 @@ -93,7 +93,7 @@ TEST(c_api, solve_time_bb_preemption) CUOPT_SUCCESS); EXPECT_EQ(termination_status, CUOPT_TERIMINATION_STATUS_OPTIMAL); EXPECT_GT(solve_time, 0); // solve time should not be equal to 0, even on very simple instances - // solved by B&B before the diversity solver has time to run + // solved by B&B before the diversity solver has time to run } TEST(c_api, bad_parameter_name) { EXPECT_EQ(test_bad_parameter_name(), CUOPT_INVALID_ARGUMENT); } @@ -112,3 +112,11 @@ TEST(c_api, test_ranged_problem) EXPECT_EQ(termination_status, CUOPT_TERIMINATION_STATUS_OPTIMAL); EXPECT_NEAR(objective, 32.0, 1e-3); } + +TEST(c_api, test_invalid_bounds) +{ + // Test LP codepath + EXPECT_EQ(test_invalid_bounds(false), CUOPT_SUCCESS); + // Test MIP codepath + EXPECT_EQ(test_invalid_bounds(true), CUOPT_SUCCESS); +} 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 e5c9a965c..0400a62e9 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 @@ -39,6 +39,7 @@ cuopt_int_t test_missing_file(); cuopt_int_t test_infeasible_problem(); cuopt_int_t test_bad_parameter_name(); cuopt_int_t test_ranged_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr); +cuopt_int_t test_invalid_bounds(cuopt_int_t test_mip); #ifdef __cplusplus } diff --git a/cpp/tests/mip/termination_test.cu b/cpp/tests/mip/termination_test.cu index 54be62c59..ce2003c40 100644 --- a/cpp/tests/mip/termination_test.cu +++ b/cpp/tests/mip/termination_test.cu @@ -110,6 +110,12 @@ TEST(termination_status, lower_bound_bb_timeout) EXPECT_GE(lb, obj_val); } +TEST(termination_status, crossing_bounds_infeasible) +{ + auto [termination_status, obj_val, lb] = test_mps_file("mip/crossing_var_bounds.mps", 0.5, false); + EXPECT_EQ(termination_status, mip_termination_status_t::Infeasible); +} + TEST(termination_status, bb_infeasible_test) { // First, check that presolve doesn't reduce the problem to infeasibility diff --git a/cpp/tests/mip/unit_test.cu b/cpp/tests/mip/unit_test.cu index b7eb24222..1114c7aba 100644 --- a/cpp/tests/mip/unit_test.cu +++ b/cpp/tests/mip/unit_test.cu @@ -159,7 +159,7 @@ TEST(ErrorTest, TestError) // Set constraint bounds std::vector lower_bounds = {1.0}; - std::vector upper_bounds = {0.0}; + std::vector upper_bounds = {1.0, 1.0}; problem.set_constraint_lower_bounds(lower_bounds.data(), lower_bounds.size()); problem.set_constraint_upper_bounds(upper_bounds.data(), upper_bounds.size()); diff --git a/datasets/mip/crossing_var_bounds.mps b/datasets/mip/crossing_var_bounds.mps new file mode 100644 index 000000000..3ed790bbc --- /dev/null +++ b/datasets/mip/crossing_var_bounds.mps @@ -0,0 +1,27 @@ +* Optimal solution -28 +NAME MIP_SAMPLE +ROWS + N OBJ + L C1 + L C2 + L C3 +COLUMNS + MARK0001 'MARKER' 'INTORG' + X1 OBJ -7 + X1 C1 -1 + X1 C2 5 + X1 C3 -2 + X2 OBJ -2 + X2 C1 2 + X2 C2 1 + X2 C3 -2 + MARK0001 'MARKER' 'INTEND' +RHS + RHS C1 4 + RHS C2 20 + RHS C3 -7 +BOUNDS + UP BOUND X1 10 + LO BOUNDS X1 20 + UP BOUND X2 10 +ENDATA