diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index b512944a6..93a6ce29a 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -107,6 +107,9 @@ #define CUOPT_METHOD_DUAL_SIMPLEX 2 #define CUOPT_METHOD_BARRIER 3 +/* @brief File format constants for problem I/O */ +#define CUOPT_FILE_FORMAT_MPS 0 + /* @brief Status codes constants */ #define CUOPT_SUCCESS 0 #define CUOPT_INVALID_ARGUMENT 1 diff --git a/cpp/include/cuopt/linear_programming/cuopt_c.h b/cpp/include/cuopt/linear_programming/cuopt_c.h index 06af2ae86..3381d3e17 100644 --- a/cpp/include/cuopt/linear_programming/cuopt_c.h +++ b/cpp/include/cuopt/linear_programming/cuopt_c.h @@ -111,6 +111,20 @@ cuopt_int_t cuOptGetVersion(cuopt_int_t* version_major, */ cuopt_int_t cuOptReadProblem(const char* filename, cuOptOptimizationProblem* problem_ptr); +/** + * @brief Write an optimization problem to a file. + * + * @param[in] problem - The optimization problem to write. + * @param[in] filename - The path to the output file. + * @param[in] format - The file format to use. Currently only CUOPT_FILE_FORMAT_MPS is supported. + * + * @return A status code indicating success or failure. Returns CUOPT_INVALID_ARGUMENT + * if an unsupported format is specified. + */ +cuopt_int_t cuOptWriteProblem(cuOptOptimizationProblem problem, + const char* filename, + cuopt_int_t format); + /** @brief Create an optimization problem of the form * * @verbatim @@ -681,6 +695,53 @@ cuopt_int_t cuOptGetFloatParameter(cuOptSolverSettings settings, const char* parameter_name, cuopt_float_t* parameter_value); +/** + * @brief Set the initial primal solution for an LP solve. + * + * @param[in] settings - The solver settings object. + * @param[in] primal_solution - A pointer to an array of type cuopt_float_t + * of size num_variables containing the initial primal values. + * @param[in] num_variables - The number of variables (size of the primal_solution array). + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptSetInitialPrimalSolution(cuOptSolverSettings settings, + const cuopt_float_t* primal_solution, + cuopt_int_t num_variables); + +/** + * @brief Set the initial dual solution for an LP solve. + * + * @param[in] settings - The solver settings object. + * @param[in] dual_solution - A pointer to an array of type cuopt_float_t + * of size num_constraints containing the initial dual values. + * @param[in] num_constraints - The number of constraints (size of the dual_solution array). + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptSetInitialDualSolution(cuOptSolverSettings settings, + const cuopt_float_t* dual_solution, + cuopt_int_t num_constraints); + +/** + * @brief Add an initial solution (MIP start) for MIP solving. + * + * This function can be called multiple times to add multiple MIP starts. + * The solver will use these as starting points for the MIP search. + * + * @param[in] settings - The solver settings object. + * @param[in] solution - A pointer to an array of type cuopt_float_t + * of size num_variables containing the solution values. + * @param[in] num_variables - The number of variables (size of the solution array). + * + * @attention Currently unsupported with presolve on. + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptAddMIPStart(cuOptSolverSettings settings, + const cuopt_float_t* solution, + cuopt_int_t num_variables); + /** @brief Check if an optimization problem is a mixed integer programming problem. * * @param[in] problem - The optimization problem. diff --git a/cpp/include/cuopt/linear_programming/optimization_problem.hpp b/cpp/include/cuopt/linear_programming/optimization_problem.hpp index dd912ec94..0d3319124 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem.hpp @@ -105,6 +105,20 @@ class optimization_problem_t { optimization_problem_t(raft::handle_t const* handle_ptr); optimization_problem_t(const optimization_problem_t& other); + /** + * @brief Check if this optimization problem is equivalent to another. + * + * Two problems are considered equivalent if they represent the same mathematical + * optimization problem, potentially with variables and constraints in a different order. + * The mapping between problems is determined by matching variable names and row names. + * Essentially checks for graph isomorphism given label mappings. + * + * @param other The other optimization problem to compare against. + * @return true if the problems are equivalent (up to permutation of variables/constraints), + * false otherwise. + */ + bool is_equivalent(const optimization_problem_t& other) const; + std::vector mip_callbacks_; /** diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index 5ec0052ce..d303f8985 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -138,6 +138,7 @@ void mps_writer_t::write(const std::string& mps_file_path) // Keep a single integer section marker by going over constraints twice and writing out // integral/nonintegral nonzeros ordered map + std::vector var_in_constraint(n_variables, false); std::map>> integral_col_nnzs; std::map>> continuous_col_nnzs; for (size_t row_id = 0; row_id < (size_t)n_constraints; row_id++) { @@ -150,12 +151,37 @@ void mps_writer_t::write(const std::string& mps_file_path) } else { continuous_col_nnzs[var].emplace_back(row_id, constraint_matrix_values[k]); } + var_in_constraint[var] = true; + } + } + + // Record and explicitely declared variables not contained in any constraint. + std::vector orphan_continuous_vars; + std::vector orphan_integer_vars; + for (i_t var = 0; var < n_variables; ++var) { + if (!var_in_constraint[var]) { + if (variable_types[var] == 'I') { + orphan_integer_vars.push_back(var); + } else { + orphan_continuous_vars.push_back(var); + } } } for (size_t is_integral = 0; is_integral < 2; is_integral++) { - auto& col_map = is_integral ? integral_col_nnzs : continuous_col_nnzs; + auto& col_map = is_integral ? integral_col_nnzs : continuous_col_nnzs; + auto& orphan_vars = is_integral ? orphan_integer_vars : orphan_continuous_vars; if (is_integral) mps_file << " MARK0001 'MARKER' 'INTORG'\n"; + for (auto& var_id : orphan_vars) { + std::string col_name = var_id < problem_.get_variable_names().size() + ? problem_.get_variable_names()[var_id] + : "C" + std::to_string(var_id); + // Write that column even if it is orphan as has a zero objective coefficient. + // Some tools require variables to be declared in "COLUMNS" before any "BOUNDS" statements. + mps_file << " " << col_name << " " + << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) + << " " << objective_coefficients[var_id] << "\n"; + } for (auto& [var_id, nnzs] : col_map) { std::string col_name = var_id < problem_.get_variable_names().size() ? problem_.get_variable_names()[var_id] @@ -222,24 +248,34 @@ void mps_writer_t::write(const std::string& mps_file_path) // BOUNDS section mps_file << "BOUNDS\n"; for (size_t j = 0; j < (size_t)n_variables; j++) { - std::string col_name = j < problem_.get_variable_names().size() - ? problem_.get_variable_names()[j] - : "C" + std::to_string(j); + std::string col_name = j < problem_.get_variable_names().size() + ? problem_.get_variable_names()[j] + : "C" + std::to_string(j); + std::string lower_bound_str = variable_types[j] == 'I' ? "LI" : "LO"; + std::string upper_bound_str = variable_types[j] == 'I' ? "UI" : "UP"; if (variable_lower_bounds[j] == -std::numeric_limits::infinity() && variable_upper_bounds[j] == std::numeric_limits::infinity()) { mps_file << " FR BOUND1 " << col_name << "\n"; + } + // Ambiguity exists in the spec about the case where upper_bound == 0 and lower_bound == 0, and + // only UP is specified. Handle fixed variables explicitely to avoid this pitfall. + else if (variable_lower_bounds[j] == variable_upper_bounds[j]) { + mps_file << " FX BOUND1 " << col_name << " " << variable_lower_bounds[j] << "\n"; } else { - if (variable_lower_bounds[j] != 0.0 || objective_coefficients[j] == 0.0 || - variable_types[j] != 'C') { + if (variable_lower_bounds[j] != 0.0) { if (variable_lower_bounds[j] == -std::numeric_limits::infinity()) { mps_file << " MI BOUND1 " << col_name << "\n"; } else { - mps_file << " LO BOUND1 " << col_name << " " << variable_lower_bounds[j] << "\n"; + mps_file << " " << lower_bound_str << " BOUND1 " << col_name << " " + << variable_lower_bounds[j] << "\n"; } } - if (variable_upper_bounds[j] != std::numeric_limits::infinity()) { - mps_file << " UP BOUND1 " << col_name << " " << variable_upper_bounds[j] << "\n"; + // Integer variables get different default bounds compared to continuous variables + if (variable_upper_bounds[j] != std::numeric_limits::infinity() || + variable_types[j] == 'I') { + mps_file << " " << upper_bound_str << " BOUND1 " << col_name << " " + << variable_upper_bounds[j] << "\n"; } } } diff --git a/cpp/src/linear_programming/cuopt_c.cpp b/cpp/src/linear_programming/cuopt_c.cpp index 0772dd14b..96593712c 100644 --- a/cpp/src/linear_programming/cuopt_c.cpp +++ b/cpp/src/linear_programming/cuopt_c.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -24,31 +25,6 @@ using namespace cuopt::mps_parser; using namespace cuopt::linear_programming; -struct problem_and_stream_view_t { - problem_and_stream_view_t() - : op_problem(nullptr), stream_view(rmm::cuda_stream_per_thread), handle(stream_view) - { - } - raft::handle_t* get_handle_ptr() { return &handle; } - cuopt::linear_programming::optimization_problem_t* op_problem; - rmm::cuda_stream_view stream_view; - raft::handle_t handle; -}; - -struct solution_and_stream_view_t { - solution_and_stream_view_t(bool solution_for_mip, rmm::cuda_stream_view stream_view) - : is_mip(solution_for_mip), - mip_solution_ptr(nullptr), - lp_solution_ptr(nullptr), - stream_view(stream_view) - { - } - bool is_mip; - mip_solution_t* mip_solution_ptr; - optimization_problem_solution_t* lp_solution_ptr; - rmm::cuda_stream_view stream_view; -}; - int8_t cuOptGetFloatSize() { return sizeof(cuopt_float_t); } int8_t cuOptGetIntSize() { return sizeof(cuopt_int_t); } @@ -92,6 +68,26 @@ cuopt_int_t cuOptReadProblem(const char* filename, cuOptOptimizationProblem* pro return CUOPT_SUCCESS; } +cuopt_int_t cuOptWriteProblem(cuOptOptimizationProblem problem, + const char* filename, + cuopt_int_t format) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (filename == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (strlen(filename) == 0) { return CUOPT_INVALID_ARGUMENT; } + if (format != CUOPT_FILE_FORMAT_MPS) { return CUOPT_INVALID_ARGUMENT; } + + problem_and_stream_view_t* problem_and_stream_view = + static_cast(problem); + try { + problem_and_stream_view->op_problem->write_to_mps(std::string(filename)); + } catch (const std::exception& e) { + CUOPT_LOG_INFO("Error writing MPS file: %s", e.what()); + return CUOPT_MPS_FILE_ERROR; + } + return CUOPT_SUCCESS; +} + cuopt_int_t cuOptCreateProblem(cuopt_int_t num_constraints, cuopt_int_t num_variables, cuopt_int_t objective_sense, @@ -706,6 +702,60 @@ cuopt_int_t cuOptGetFloatParameter(cuOptSolverSettings settings, return CUOPT_SUCCESS; } +cuopt_int_t cuOptSetInitialPrimalSolution(cuOptSolverSettings settings, + const cuopt_float_t* primal_solution, + cuopt_int_t num_variables) +{ + if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (primal_solution == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (num_variables <= 0) { return CUOPT_INVALID_ARGUMENT; } + + solver_settings_t* solver_settings = + static_cast*>(settings); + try { + solver_settings->set_initial_pdlp_primal_solution(primal_solution, num_variables); + } catch (const std::exception& e) { + return CUOPT_INVALID_ARGUMENT; + } + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptSetInitialDualSolution(cuOptSolverSettings settings, + const cuopt_float_t* dual_solution, + cuopt_int_t num_constraints) +{ + if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (dual_solution == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (num_constraints <= 0) { return CUOPT_INVALID_ARGUMENT; } + + solver_settings_t* solver_settings = + static_cast*>(settings); + try { + solver_settings->set_initial_pdlp_dual_solution(dual_solution, num_constraints); + } catch (const std::exception& e) { + return CUOPT_INVALID_ARGUMENT; + } + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptAddMIPStart(cuOptSolverSettings settings, + const cuopt_float_t* solution, + cuopt_int_t num_variables) +{ + if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (solution == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (num_variables <= 0) { return CUOPT_INVALID_ARGUMENT; } + + solver_settings_t* solver_settings = + static_cast*>(settings); + try { + solver_settings->get_mip_settings().add_initial_solution(solution, num_variables); + } catch (const std::exception& e) { + return CUOPT_INVALID_ARGUMENT; + } + return CUOPT_SUCCESS; +} + cuopt_int_t cuOptIsMIP(cuOptOptimizationProblem problem, cuopt_int_t* is_mip_ptr) { if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } diff --git a/cpp/src/linear_programming/cuopt_c_internal.hpp b/cpp/src/linear_programming/cuopt_c_internal.hpp new file mode 100644 index 000000000..a141ca666 --- /dev/null +++ b/cpp/src/linear_programming/cuopt_c_internal.hpp @@ -0,0 +1,46 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include + +namespace cuopt::linear_programming { + +struct problem_and_stream_view_t { + problem_and_stream_view_t() + : op_problem(nullptr), stream_view(rmm::cuda_stream_per_thread), handle(stream_view) + { + } + raft::handle_t* get_handle_ptr() { return &handle; } + optimization_problem_t* op_problem; + rmm::cuda_stream_view stream_view; + raft::handle_t handle; +}; + +struct solution_and_stream_view_t { + solution_and_stream_view_t(bool solution_for_mip, rmm::cuda_stream_view stream_view) + : is_mip(solution_for_mip), + mip_solution_ptr(nullptr), + lp_solution_ptr(nullptr), + stream_view(stream_view) + { + } + bool is_mip; + mip_solution_t* mip_solution_ptr; + optimization_problem_solution_t* lp_solution_ptr; + rmm::cuda_stream_view stream_view; +}; + +} // namespace cuopt::linear_programming diff --git a/cpp/src/linear_programming/optimization_problem.cu b/cpp/src/linear_programming/optimization_problem.cu index 72d75cdc7..eab967edc 100644 --- a/cpp/src/linear_programming/optimization_problem.cu +++ b/cpp/src/linear_programming/optimization_problem.cu @@ -17,11 +17,21 @@ #include #include +#include + +#include #include +#include +#include +#include +#include +#include +#include #include #include +#include namespace cuopt::linear_programming { @@ -78,6 +88,251 @@ optimization_problem_t::optimization_problem_t( { } +/** + * @brief Compare two CSR matrices for equivalence under row and column permutations. + * + * @param this_offsets Row offsets of first matrix + * @param this_indices Column indices of first matrix + * @param this_values Values of first matrix + * @param other_offsets Row offsets of second matrix + * @param other_indices Column indices of second matrix + * @param other_values Values of second matrix + * @param d_row_perm_inv Inverse row permutation (maps other's row indices to this's) + * @param d_col_perm_inv Inverse column permutation (maps other's col indices to this's) + * @param n_cols Number of columns (used for sort key computation) + * @param stream CUDA stream + * @return true if matrices are equivalent under the given permutations + */ +template +static bool csr_matrices_equivalent_with_permutation(const rmm::device_uvector& this_offsets, + const rmm::device_uvector& this_indices, + const rmm::device_uvector& this_values, + const rmm::device_uvector& other_offsets, + const rmm::device_uvector& other_indices, + const rmm::device_uvector& other_values, + const rmm::device_uvector& d_row_perm_inv, + const rmm::device_uvector& d_col_perm_inv, + i_t n_cols, + rmm::cuda_stream_view stream) +{ + const i_t nnz = static_cast(this_values.size()); + if (nnz != static_cast(other_values.size())) { return false; } + if (nnz == 0) { return true; } + + auto policy = rmm::exec_policy(stream); + + // Expand CSR row offsets to row indices for 'this' + rmm::device_uvector this_rows(nnz, stream); + rmm::device_uvector this_cols(nnz, stream); + rmm::device_uvector this_vals(nnz, stream); + + rmm::device_uvector entry_indices(nnz, stream); + thrust::sequence(policy, entry_indices.begin(), entry_indices.end()); + + thrust::upper_bound(policy, + this_offsets.begin(), + this_offsets.end(), + entry_indices.begin(), + entry_indices.end(), + this_rows.begin()); + thrust::transform( + policy, this_rows.begin(), this_rows.end(), this_rows.begin(), [] __device__(i_t r) { + return r - 1; + }); + + thrust::copy(policy, this_indices.begin(), this_indices.end(), this_cols.begin()); + thrust::copy(policy, this_values.begin(), this_values.end(), this_vals.begin()); + + // For 'other': expand and apply inverse permutations to map to 'this' coordinate system + rmm::device_uvector other_rows(nnz, stream); + rmm::device_uvector other_cols(nnz, stream); + rmm::device_uvector other_vals(nnz, stream); + + thrust::upper_bound(policy, + other_offsets.begin(), + other_offsets.end(), + entry_indices.begin(), + entry_indices.end(), + other_rows.begin()); + thrust::transform( + policy, other_rows.begin(), other_rows.end(), other_rows.begin(), [] __device__(i_t r) { + return r - 1; + }); + + thrust::gather( + policy, other_rows.begin(), other_rows.end(), d_row_perm_inv.begin(), other_rows.begin()); + + thrust::gather( + policy, other_indices.begin(), other_indices.end(), d_col_perm_inv.begin(), other_cols.begin()); + + thrust::copy(policy, other_values.begin(), other_values.end(), other_vals.begin()); + + // Create sort keys: row * n_cols + col (to sort by row then column) + rmm::device_uvector this_keys(nnz, stream); + rmm::device_uvector other_keys(nnz, stream); + + const int64_t n_cols_64 = n_cols; + thrust::transform(policy, + thrust::make_zip_iterator(this_rows.begin(), this_cols.begin()), + thrust::make_zip_iterator(this_rows.end(), this_cols.end()), + this_keys.begin(), + [n_cols_64] __device__(thrust::tuple rc) { + return static_cast(thrust::get<0>(rc)) * n_cols_64 + + static_cast(thrust::get<1>(rc)); + }); + + thrust::transform(policy, + thrust::make_zip_iterator(other_rows.begin(), other_cols.begin()), + thrust::make_zip_iterator(other_rows.end(), other_cols.end()), + other_keys.begin(), + [n_cols_64] __device__(thrust::tuple rc) { + return static_cast(thrust::get<0>(rc)) * n_cols_64 + + static_cast(thrust::get<1>(rc)); + }); + + thrust::sort_by_key(policy, this_keys.begin(), this_keys.end(), this_vals.begin()); + thrust::sort_by_key(policy, other_keys.begin(), other_keys.end(), other_vals.begin()); + + if (!thrust::equal(policy, this_keys.begin(), this_keys.end(), other_keys.begin())) { + return false; + } + + if (!thrust::equal(policy, this_vals.begin(), this_vals.end(), other_vals.begin())) { + return false; + } + + return true; +} + +template +bool optimization_problem_t::is_equivalent( + const optimization_problem_t& other) const +{ + if (maximize_ != other.maximize_) { return false; } + if (n_vars_ != other.n_vars_) { return false; } + if (n_constraints_ != other.n_constraints_) { return false; } + if (objective_scaling_factor_ != other.objective_scaling_factor_) { return false; } + if (objective_offset_ != other.objective_offset_) { return false; } + if (problem_category_ != other.problem_category_) { return false; } + if (A_.size() != other.A_.size()) { return false; } + + if (var_names_.empty() || other.var_names_.empty()) { return false; } + if (row_names_.empty() || other.row_names_.empty()) { return false; } + + // Build variable permutation: var_perm[i] = index j in other where var_names_[i] == + // other.var_names_[j] + std::unordered_map other_var_idx; + for (size_t j = 0; j < other.var_names_.size(); ++j) { + other_var_idx[other.var_names_[j]] = static_cast(j); + } + std::vector var_perm(n_vars_); + for (i_t i = 0; i < n_vars_; ++i) { + auto it = other_var_idx.find(var_names_[i]); + if (it == other_var_idx.end()) { return false; } + var_perm[i] = it->second; + } + + // Build row permutation: row_perm[i] = index j in other where row_names_[i] == + // other.row_names_[j] + std::unordered_map other_row_idx; + for (size_t j = 0; j < other.row_names_.size(); ++j) { + other_row_idx[other.row_names_[j]] = static_cast(j); + } + std::vector row_perm(n_constraints_); + for (i_t i = 0; i < n_constraints_; ++i) { + auto it = other_row_idx.find(row_names_[i]); + if (it == other_row_idx.end()) { return false; } + row_perm[i] = it->second; + } + + // Upload permutations to GPU + rmm::device_uvector d_var_perm(n_vars_, stream_view_); + rmm::device_uvector d_row_perm(n_constraints_, stream_view_); + raft::copy(d_var_perm.data(), var_perm.data(), n_vars_, stream_view_); + raft::copy(d_row_perm.data(), row_perm.data(), n_constraints_, stream_view_); + + auto policy = rmm::exec_policy(stream_view_); + + auto permuted_eq = [&](auto this_begin, auto this_end, auto other_begin, auto perm_begin) { + auto other_perm = thrust::make_permutation_iterator(other_begin, perm_begin); + return thrust::equal(policy, this_begin, this_end, other_perm); + }; + + // Compare variable-indexed arrays + if (!permuted_eq(c_.begin(), c_.end(), other.c_.begin(), d_var_perm.begin())) { return false; } + if (!permuted_eq(variable_lower_bounds_.begin(), + variable_lower_bounds_.end(), + other.variable_lower_bounds_.begin(), + d_var_perm.begin())) { + return false; + } + if (!permuted_eq(variable_upper_bounds_.begin(), + variable_upper_bounds_.end(), + other.variable_upper_bounds_.begin(), + d_var_perm.begin())) { + return false; + } + if (!permuted_eq(variable_types_.begin(), + variable_types_.end(), + other.variable_types_.begin(), + d_var_perm.begin())) { + return false; + } + + // Compare constraint-indexed arrays + if (!permuted_eq(b_.begin(), b_.end(), other.b_.begin(), d_row_perm.begin())) { return false; } + if (!permuted_eq(constraint_lower_bounds_.begin(), + constraint_lower_bounds_.end(), + other.constraint_lower_bounds_.begin(), + d_row_perm.begin())) { + return false; + } + if (!permuted_eq(constraint_upper_bounds_.begin(), + constraint_upper_bounds_.end(), + other.constraint_upper_bounds_.begin(), + d_row_perm.begin())) { + return false; + } + if (!permuted_eq( + row_types_.begin(), row_types_.end(), other.row_types_.begin(), d_row_perm.begin())) { + return false; + } + + // Build inverse permutations on CPU (needed for CSR comparisons) + std::vector var_perm_inv(n_vars_); + for (i_t i = 0; i < n_vars_; ++i) { + var_perm_inv[var_perm[i]] = i; + } + std::vector row_perm_inv(n_constraints_); + for (i_t i = 0; i < n_constraints_; ++i) { + row_perm_inv[row_perm[i]] = i; + } + + // Upload inverse permutations to GPU + rmm::device_uvector d_var_perm_inv(n_vars_, stream_view_); + rmm::device_uvector d_row_perm_inv(n_constraints_, stream_view_); + raft::copy(d_var_perm_inv.data(), var_perm_inv.data(), n_vars_, stream_view_); + raft::copy(d_row_perm_inv.data(), row_perm_inv.data(), n_constraints_, stream_view_); + + // Constraint matrix (A) comparison with row and column permutations + if (!csr_matrices_equivalent_with_permutation(A_offsets_, + A_indices_, + A_, + other.A_offsets_, + other.A_indices_, + other.A_, + d_row_perm_inv, + d_var_perm_inv, + n_vars_, + stream_view_)) { + return false; + } + + // Q matrix writing to MPS not supported yet. Don't check for equivalence here + + return true; +} + template void optimization_problem_t::set_csr_constraint_matrix(const f_t* A_values, i_t size_values, diff --git a/cpp/tests/linear_programming/CMakeLists.txt b/cpp/tests/linear_programming/CMakeLists.txt index c091751f9..295044355 100644 --- a/cpp/tests/linear_programming/CMakeLists.txt +++ b/cpp/tests/linear_programming/CMakeLists.txt @@ -52,6 +52,10 @@ if (NOT SKIP_C_PYTHON_ADAPTERS) ${CUOPT_PRIVATE_CUDA_LIBS} ) + if(NOT DEFINED INSTALL_TARGET OR "${INSTALL_TARGET}" STREQUAL "") + target_link_options(C_API_TEST PRIVATE -Wl,--enable-new-dtags) + endif() + add_test(NAME C_API_TEST COMMAND C_API_TEST) install( 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 17f644ab0..12ac890bc 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 @@ -1200,3 +1200,84 @@ cuOptDestroySolution(&solution); return status; } + +cuopt_int_t test_write_problem(const char* input_filename, const char* output_filename) +{ + cuOptOptimizationProblem problem = NULL; + cuOptOptimizationProblem problem_read = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + cuopt_int_t status; + cuopt_int_t termination_status; + cuopt_float_t objective_value; + + /* Read the input problem */ + status = cuOptReadProblem(input_filename, &problem); + if (status != CUOPT_SUCCESS) { + printf("Error reading problem from %s: %d\n", input_filename, status); + goto DONE; + } + + /* Write the problem to MPS file */ + status = cuOptWriteProblem(problem, output_filename, CUOPT_FILE_FORMAT_MPS); + if (status != CUOPT_SUCCESS) { + printf("Error writing problem to MPS: %d\n", status); + goto DONE; + } + printf("Problem written to %s\n", output_filename); + + /* Read the problem back */ + status = cuOptReadProblem(output_filename, &problem_read); + if (status != CUOPT_SUCCESS) { + printf("Error reading problem from MPS: %d\n", status); + goto DONE; + } + printf("Problem read back from %s\n", output_filename); + + status = cuOptCreateSolverSettings(&settings); + if (status != CUOPT_SUCCESS) { + printf("Error creating solver settings: %d\n", status); + goto DONE; + } + + status = cuOptSetIntegerParameter(settings, CUOPT_METHOD, CUOPT_METHOD_PDLP); + if (status != CUOPT_SUCCESS) { + printf("Error setting method: %d\n", status); + goto DONE; + } + + status = cuOptSolve(problem_read, settings, &solution); + if (status != CUOPT_SUCCESS) { + printf("Error solving problem: %d\n", status); + goto DONE; + } + + status = cuOptGetTerminationStatus(solution, &termination_status); + if (status != CUOPT_SUCCESS) { + printf("Error getting termination status: %d\n", status); + goto DONE; + } + + status = cuOptGetObjectiveValue(solution, &objective_value); + if (status != CUOPT_SUCCESS) { + printf("Error getting objective value: %d\n", status); + goto DONE; + } + + printf("Termination status: %d, Objective: %f\n", termination_status, objective_value); + + if (termination_status != CUOPT_TERIMINATION_STATUS_OPTIMAL) { + printf("Expected optimal status\n"); + status = -1; + goto DONE; + } + + printf("Write problem test passed\n"); + +DONE: + cuOptDestroyProblem(&problem); + cuOptDestroyProblem(&problem_read); + 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 af1295298..2510a9e56 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 @@ -7,7 +7,11 @@ #include "c_api_tests.h" +#include +#include + #include +#include #include #include @@ -128,3 +132,92 @@ TEST(c_api, test_quadratic_ranged_problem) EXPECT_EQ(termination_status, (int)CUOPT_TERIMINATION_STATUS_OPTIMAL); EXPECT_NEAR(objective, -32.0, 1e-3); } + +TEST(c_api, test_write_problem) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + std::string input_file = rapidsDatasetRootDir + "/linear_programming/afiro_original.mps"; + std::string temp_file = std::filesystem::temp_directory_path().string() + "/c_api_test_write.mps"; + EXPECT_EQ(test_write_problem(input_file.c_str(), temp_file.c_str()), CUOPT_SUCCESS); + std::filesystem::remove(temp_file); +} + +static bool test_mps_roundtrip(const std::string& mps_file_path) +{ + using cuopt::linear_programming::problem_and_stream_view_t; + + cuOptOptimizationProblem original_handle = nullptr; + cuOptOptimizationProblem reread_handle = nullptr; + bool result = false; + + std::string model_basename = std::filesystem::path(mps_file_path).filename().string(); + std::string temp_file = + std::filesystem::temp_directory_path().string() + "/roundtrip_temp_" + model_basename; + + if (cuOptReadProblem(mps_file_path.c_str(), &original_handle) != CUOPT_SUCCESS) { + std::cerr << "Failed to read original MPS file: " << mps_file_path << std::endl; + goto cleanup; + } + + if (cuOptWriteProblem(original_handle, temp_file.c_str(), CUOPT_FILE_FORMAT_MPS) != + CUOPT_SUCCESS) { + std::cerr << "Failed to write MPS file: " << temp_file << std::endl; + goto cleanup; + } + + if (cuOptReadProblem(temp_file.c_str(), &reread_handle) != CUOPT_SUCCESS) { + std::cerr << "Failed to re-read MPS file: " << temp_file << std::endl; + goto cleanup; + } + + { + auto* original_problem_wrapper = static_cast(original_handle); + auto* reread_problem_wrapper = static_cast(reread_handle); + + result = + original_problem_wrapper->op_problem->is_equivalent(*reread_problem_wrapper->op_problem); + } + +cleanup: + std::filesystem::remove(temp_file); + cuOptDestroyProblem(&original_handle); + cuOptDestroyProblem(&reread_handle); + + return result; +} + +class WriteRoundtripTestFixture : public ::testing::TestWithParam {}; +TEST_P(WriteRoundtripTestFixture, roundtrip) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + EXPECT_TRUE(test_mps_roundtrip(rapidsDatasetRootDir + GetParam())); +} +INSTANTIATE_TEST_SUITE_P(c_api, + WriteRoundtripTestFixture, + ::testing::Values("/linear_programming/afiro_original.mps", + "/mip/50v-10.mps", + "/mip/fiball.mps", + "/mip/gen-ip054.mps", + "/mip/sct2.mps", + "/mip/uccase9.mps", + "/mip/drayage-25-23.mps", + "/mip/tr12-30.mps", + "/mip/neos-3004026-krka.mps", + "/mip/ns1208400.mps", + "/mip/gmu-35-50.mps", + "/mip/n2seq36q.mps", + "/mip/seymour1.mps", + "/mip/rmatr200-p5.mps", + "/mip/cvs16r128-89.mps", + "/mip/thor50dday.mps", + "/mip/stein9inf.mps", + "/mip/neos5.mps", + "/mip/neos5-free-bound.mps", + "/mip/crossing_var_bounds.mps", + "/mip/cod105_max.mps", + "/mip/sudoku.mps", + "/mip/presolve-infeasible.mps", + "/mip/swath1.mps", + "/mip/enlight_hard.mps", + "/mip/enlight11.mps", + "/mip/supportcase22.mps")); 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 5726c3a99..d2ee03a8a 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 @@ -34,7 +34,7 @@ cuopt_int_t test_quadratic_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr); cuopt_int_t test_quadratic_ranged_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr); - +cuopt_int_t test_write_problem(const char* input_filename, const char* output_filename); #ifdef __cplusplus } #endif