diff --git a/cpp/include/cuopt/linear_programming/optimization_problem.hpp b/cpp/include/cuopt/linear_programming/optimization_problem.hpp index db6172367..6d9ba5cda 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem.hpp @@ -304,6 +304,13 @@ class optimization_problem_t { */ void set_row_names(const std::vector& row_names); + /** + * @brief Write the problem to an MPS formatted file + * + * @param[in] mps_file_path Path to the MPS file to write + */ + void write_to_mps(const std::string& mps_file_path); + i_t get_n_variables() const; i_t get_n_constraints() const; i_t get_nnz() const; diff --git a/cpp/include/cuopt/linear_programming/solve.hpp b/cpp/include/cuopt/linear_programming/solve.hpp index 04ee5530c..11e8f9dcf 100644 --- a/cpp/include/cuopt/linear_programming/solve.hpp +++ b/cpp/include/cuopt/linear_programming/solve.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace cuopt::linear_programming { diff --git a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp index eef185d0d..46d672cb1 100644 --- a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp @@ -22,10 +22,10 @@ #include #include #include +#include #include #include - -#include +#include #include #include diff --git a/cpp/libmps_parser/CMakeLists.txt b/cpp/libmps_parser/CMakeLists.txt index d0cbd8a29..af09ebd2c 100644 --- a/cpp/libmps_parser/CMakeLists.txt +++ b/cpp/libmps_parser/CMakeLists.txt @@ -78,7 +78,9 @@ add_library(mps_parser SHARED src/data_model_view.cpp src/mps_data_model.cpp src/mps_parser.cpp + src/mps_writer.cpp src/parser.cpp + src/writer.cpp src/utilities/cython_mps_parser.cpp ) diff --git a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp index 17f74a6c2..6b32d4fae 100644 --- a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp +++ b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp @@ -186,6 +186,20 @@ class data_model_view_t { * @param[in] problem_name Problem name value. */ void set_problem_name(const std::string& problem_name); + /** + * @brief Set the variables names. + * @note Setting before calling the solver is optional. + * + * @param[in] variable_names Variable names values. + */ + void set_variable_names(const std::vector& variables_names); + /** + * @brief Set the row names. + * @note Setting before calling the solver is optional. + * + * @param[in] row_names Row names value. + */ + void set_row_names(const std::vector& row_names); /** * @brief Set the constraints lower bounds. * @note Setting before calling the solver is optional if you set the row type, else it's @@ -350,6 +364,19 @@ class data_model_view_t { */ span get_initial_dual_solution() const noexcept; + /** + * @brief Get the variable names + * + * @return span + */ + const std::vector& get_variable_names() const noexcept; + /** + * @brief Get the row names + * + * @return span + */ + const std::vector& get_row_names() const noexcept; + /** * @brief Get the problem name * @@ -404,6 +431,8 @@ class data_model_view_t { span row_types_; std::string objective_name_; std::string problem_name_; + std::vector variable_names_; + std::vector row_names_; span constraint_lower_bounds_; span constraint_upper_bounds_; diff --git a/cpp/libmps_parser/include/mps_parser/mps_writer.hpp b/cpp/libmps_parser/include/mps_parser/mps_writer.hpp new file mode 100644 index 000000000..da919a68e --- /dev/null +++ b/cpp/libmps_parser/include/mps_parser/mps_writer.hpp @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace cuopt::mps_parser { + +/** + * @brief Main writer class for MPS files + * + * @tparam f_t data type of the weights and variables + * @tparam i_t data type of the indices + */ +template +class mps_writer_t { + public: + /** + * @brief Ctor. Takes a data model view as input and writes it out as a MPS formatted file + * + * @param[in] problem Data model view to write + * @param[in] file Path to the MPS file to write + */ + mps_writer_t(const data_model_view_t& problem); + + /** + * @brief Writes the problem to an MPS formatted file + * + * @param[in] mps_file_path Path to the MPS file to write + */ + void write(const std::string& mps_file_path); + + private: + const data_model_view_t& problem_; +}; // class mps_writer_t + +} // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/writer.hpp b/cpp/libmps_parser/include/mps_parser/writer.hpp new file mode 100644 index 000000000..8f193af13 --- /dev/null +++ b/cpp/libmps_parser/include/mps_parser/writer.hpp @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +// TODO: we might want to eventually rename libmps_parser to libmps_io +// (or libcuopt_io if we want to support other hypothetical formats) +namespace cuopt::mps_parser { + +/** + * @brief Writes the problem to an MPS formatted file + * + * Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more + * details on both free and fixed MPS format. + * + * @param[in] problem The problem data model view to write + * @param[in] mps_file_path Path to the MPS file to write + */ +template +void write_mps(const data_model_view_t& problem, const std::string& mps_file_path); + +} // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/src/data_model_view.cpp b/cpp/libmps_parser/src/data_model_view.cpp index efbe1a0f2..25558e37e 100644 --- a/cpp/libmps_parser/src/data_model_view.cpp +++ b/cpp/libmps_parser/src/data_model_view.cpp @@ -198,6 +198,19 @@ void data_model_view_t::set_problem_name(const std::string& problem_na problem_name_ = problem_name; } +template +void data_model_view_t::set_variable_names( + const std::vector& variables_names) +{ + variable_names_ = variables_names; +} + +template +void data_model_view_t::set_row_names(const std::vector& row_names) +{ + row_names_ = row_names; +} + template span data_model_view_t::get_constraint_matrix_values() const noexcept { @@ -306,6 +319,18 @@ bool data_model_view_t::get_sense() const noexcept return maximize_; } +template +const std::vector& data_model_view_t::get_variable_names() const noexcept +{ + return variable_names_; +} + +template +const std::vector& data_model_view_t::get_row_names() const noexcept +{ + return row_names_; +} + // QPS-specific getter implementations template span data_model_view_t::get_quadratic_objective_values() const noexcept diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp new file mode 100644 index 000000000..7eb1a4f42 --- /dev/null +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -0,0 +1,264 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace cuopt::mps_parser { + +template +mps_writer_t::mps_writer_t(const data_model_view_t& problem) : problem_(problem) +{ +} + +template +void mps_writer_t::write(const std::string& mps_file_path) +{ + std::ofstream mps_file(mps_file_path); + + mps_parser_expects(mps_file.is_open(), + error_type_t::ValidationError, + "Error creating output MPS file! Given path: %s", + mps_file_path.c_str()); + + i_t n_variables = problem_.get_variable_lower_bounds().size(); + i_t n_constraints; + if (problem_.get_constraint_bounds().size() > 0) + n_constraints = problem_.get_constraint_bounds().size(); + else + n_constraints = problem_.get_constraint_lower_bounds().size(); + + std::vector objective_coefficients(problem_.get_objective_coefficients().size()); + std::vector constraint_lower_bounds(n_constraints); + std::vector constraint_upper_bounds(n_constraints); + std::vector constraint_bounds(problem_.get_constraint_bounds().size()); + std::vector variable_lower_bounds(problem_.get_variable_lower_bounds().size()); + std::vector variable_upper_bounds(problem_.get_variable_upper_bounds().size()); + std::vector variable_types(problem_.get_variable_types().size()); + std::vector row_types(problem_.get_row_types().size()); + std::vector constraint_matrix_offsets(problem_.get_constraint_matrix_offsets().size()); + std::vector constraint_matrix_indices(problem_.get_constraint_matrix_indices().size()); + std::vector constraint_matrix_values(problem_.get_constraint_matrix_values().size()); + + std::copy( + problem_.get_objective_coefficients().data(), + problem_.get_objective_coefficients().data() + problem_.get_objective_coefficients().size(), + objective_coefficients.data()); + std::copy(problem_.get_constraint_bounds().data(), + problem_.get_constraint_bounds().data() + problem_.get_constraint_bounds().size(), + constraint_bounds.data()); + std::copy( + problem_.get_variable_lower_bounds().data(), + problem_.get_variable_lower_bounds().data() + problem_.get_variable_lower_bounds().size(), + variable_lower_bounds.data()); + std::copy( + problem_.get_variable_upper_bounds().data(), + problem_.get_variable_upper_bounds().data() + problem_.get_variable_upper_bounds().size(), + variable_upper_bounds.data()); + std::copy(problem_.get_variable_types().data(), + problem_.get_variable_types().data() + problem_.get_variable_types().size(), + variable_types.data()); + std::copy(problem_.get_row_types().data(), + problem_.get_row_types().data() + problem_.get_row_types().size(), + row_types.data()); + std::copy(problem_.get_constraint_matrix_offsets().data(), + problem_.get_constraint_matrix_offsets().data() + + problem_.get_constraint_matrix_offsets().size(), + constraint_matrix_offsets.data()); + std::copy(problem_.get_constraint_matrix_indices().data(), + problem_.get_constraint_matrix_indices().data() + + problem_.get_constraint_matrix_indices().size(), + constraint_matrix_indices.data()); + std::copy( + problem_.get_constraint_matrix_values().data(), + problem_.get_constraint_matrix_values().data() + problem_.get_constraint_matrix_values().size(), + constraint_matrix_values.data()); + + if (problem_.get_constraint_lower_bounds().size() == 0 || + problem_.get_constraint_upper_bounds().size() == 0) { + for (size_t i = 0; i < (size_t)n_constraints; i++) { + constraint_lower_bounds[i] = constraint_bounds[i]; + constraint_upper_bounds[i] = constraint_bounds[i]; + if (row_types[i] == 'L') { + constraint_lower_bounds[i] = -std::numeric_limits::infinity(); + } else if (row_types[i] == 'G') { + constraint_upper_bounds[i] = std::numeric_limits::infinity(); + } + } + } else { + std::copy( + problem_.get_constraint_lower_bounds().data(), + problem_.get_constraint_lower_bounds().data() + problem_.get_constraint_lower_bounds().size(), + constraint_lower_bounds.data()); + std::copy( + problem_.get_constraint_upper_bounds().data(), + problem_.get_constraint_upper_bounds().data() + problem_.get_constraint_upper_bounds().size(), + constraint_upper_bounds.data()); + } + + // save coefficients with full precision + mps_file << std::setprecision(std::numeric_limits::max_digits10); + + // NAME section + mps_file << "NAME " << problem_.get_problem_name() << "\n"; + + if (problem_.get_sense()) { mps_file << "OBJSENSE\n MAXIMIZE\n"; } + + // ROWS section + mps_file << "ROWS\n"; + mps_file << " N " + << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) + << "\n"; + for (size_t i = 0; i < (size_t)n_constraints; i++) { + std::string row_name = + i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); + char type = 'L'; + if (constraint_lower_bounds[i] == constraint_upper_bounds[i]) + type = 'E'; + else if (std::isinf(constraint_upper_bounds[i])) + type = 'G'; + mps_file << " " << type << " " << row_name << "\n"; + } + + // COLUMNS section + mps_file << "COLUMNS\n"; + + // Keep a single integer section marker by going over constraints twice and writing out + // integral/nonintegral nonzeros ordered map + std::map>> integral_col_nnzs; + std::map>> continuous_col_nnzs; + for (size_t row_id = 0; row_id < (size_t)n_constraints; row_id++) { + for (size_t k = (size_t)constraint_matrix_offsets[row_id]; + k < (size_t)constraint_matrix_offsets[row_id + 1]; + k++) { + size_t var = (size_t)constraint_matrix_indices[k]; + if (variable_types[var] == 'I') { + integral_col_nnzs[var].emplace_back(row_id, constraint_matrix_values[k]); + } else { + continuous_col_nnzs[var].emplace_back(row_id, constraint_matrix_values[k]); + } + } + } + + for (size_t is_integral = 0; is_integral < 2; is_integral++) { + auto& col_map = is_integral ? integral_col_nnzs : continuous_col_nnzs; + if (is_integral) mps_file << " MARK0001 'MARKER' 'INTORG'\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] + : "C" + std::to_string(var_id); + for (auto& nnz : nnzs) { + std::string row_name = nnz.first < problem_.get_row_names().size() + ? problem_.get_row_names()[nnz.first] + : "R" + std::to_string(nnz.first); + mps_file << " " << col_name << " " << row_name << " " << nnz.second << "\n"; + } + // Write objective coefficients + if (objective_coefficients[var_id] != 0.0) { + mps_file << " " << col_name << " " + << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) + << " " << objective_coefficients[var_id] << "\n"; + } + } + if (is_integral) mps_file << " MARK0001 'MARKER' 'INTEND'\n"; + } + + // RHS section + mps_file << "RHS\n"; + for (size_t i = 0; i < (size_t)n_constraints; i++) { + std::string row_name = + i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); + + f_t rhs; + if (constraint_bounds.size() > 0) + rhs = constraint_bounds[i]; + else if (std::isinf(constraint_lower_bounds[i])) { + rhs = constraint_upper_bounds[i]; + } else if (std::isinf(constraint_upper_bounds[i])) { + rhs = constraint_lower_bounds[i]; + } else { // RANGES, encode the lower bound + rhs = constraint_lower_bounds[i]; + } + + if (std::isfinite(rhs) && rhs != 0.0) { + mps_file << " RHS1 " << row_name << " " << rhs << "\n"; + } + } + if (std::isfinite(problem_.get_objective_offset()) && problem_.get_objective_offset() != 0.0) { + mps_file << " RHS1 " + << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) + << " " << -problem_.get_objective_offset() << "\n"; + } + + // RANGES section if needed + bool has_ranges = false; + for (size_t i = 0; i < (size_t)n_constraints; i++) { + if (constraint_lower_bounds[i] != -std::numeric_limits::infinity() && + constraint_upper_bounds[i] != std::numeric_limits::infinity() && + constraint_lower_bounds[i] != constraint_upper_bounds[i]) { + if (!has_ranges) { + mps_file << "RANGES\n"; + has_ranges = true; + } + std::string row_name = "R" + std::to_string(i); + mps_file << " RNG1 " << row_name << " " + << (constraint_upper_bounds[i] - constraint_lower_bounds[i]) << "\n"; + } + } + + // 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); + + if (variable_lower_bounds[j] == -std::numeric_limits::infinity() && + variable_upper_bounds[j] == std::numeric_limits::infinity()) { + mps_file << " FR BOUND1 " << col_name << "\n"; + } else { + if (variable_lower_bounds[j] != 0.0 || objective_coefficients[j] == 0.0 || + variable_types[j] != 'C') { + 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"; + } + } + if (variable_upper_bounds[j] != std::numeric_limits::infinity()) { + mps_file << " UP BOUND1 " << col_name << " " << variable_upper_bounds[j] << "\n"; + } + } + } + + mps_file << "ENDATA\n"; + mps_file.close(); +} + +template class mps_writer_t; +template class mps_writer_t; + +} // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/src/writer.cpp b/cpp/libmps_parser/src/writer.cpp new file mode 100644 index 000000000..cf05653c3 --- /dev/null +++ b/cpp/libmps_parser/src/writer.cpp @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +namespace cuopt::mps_parser { + +template +void write_mps(const data_model_view_t& problem, const std::string& mps_file_path) +{ + mps_writer_t writer(problem); + writer.write(mps_file_path); +} + +template void write_mps(const data_model_view_t& problem, + const std::string& mps_file_path); +template void write_mps(const data_model_view_t& problem, + const std::string& mps_file_path); + +} // namespace cuopt::mps_parser diff --git a/cpp/src/linear_programming/optimization_problem.cu b/cpp/src/linear_programming/optimization_problem.cu index cdd6cf293..5847c503b 100644 --- a/cpp/src/linear_programming/optimization_problem.cu +++ b/cpp/src/linear_programming/optimization_problem.cu @@ -16,10 +16,11 @@ */ #include -#include +#include #include #include +#include #include #include @@ -490,6 +491,98 @@ void optimization_problem_t::set_maximize(bool _maximize) maximize_ = _maximize; } +template +void optimization_problem_t::write_to_mps(const std::string& mps_file_path) +{ + cuopt::mps_parser::data_model_view_t data_model_view; + + // Set optimization sense + data_model_view.set_maximize(get_sense()); + + // Copy to host + auto constraint_matrix_values = cuopt::host_copy(get_constraint_matrix_values()); + auto constraint_matrix_indices = cuopt::host_copy(get_constraint_matrix_indices()); + auto constraint_matrix_offsets = cuopt::host_copy(get_constraint_matrix_offsets()); + auto constraint_bounds = cuopt::host_copy(get_constraint_bounds()); + auto objective_coefficients = cuopt::host_copy(get_objective_coefficients()); + auto variable_lower_bounds = cuopt::host_copy(get_variable_lower_bounds()); + auto variable_upper_bounds = cuopt::host_copy(get_variable_upper_bounds()); + auto constraint_lower_bounds = cuopt::host_copy(get_constraint_lower_bounds()); + auto constraint_upper_bounds = cuopt::host_copy(get_constraint_upper_bounds()); + auto row_types = cuopt::host_copy(get_row_types()); + + // Set constraint matrix in CSR format + if (get_nnz() != 0) { + data_model_view.set_csr_constraint_matrix(constraint_matrix_values.data(), + constraint_matrix_values.size(), + constraint_matrix_indices.data(), + constraint_matrix_indices.size(), + constraint_matrix_offsets.data(), + constraint_matrix_offsets.size()); + } + + // Set constraint bounds (RHS) + if (get_n_constraints() != 0) { + data_model_view.set_constraint_bounds(constraint_bounds.data(), constraint_bounds.size()); + } + + // Set objective coefficients + if (get_n_variables() != 0) { + data_model_view.set_objective_coefficients(objective_coefficients.data(), + objective_coefficients.size()); + } + + // Set objective scaling and offset + data_model_view.set_objective_scaling_factor(get_objective_scaling_factor()); + data_model_view.set_objective_offset(get_objective_offset()); + + // Set variable bounds + if (get_n_variables() != 0) { + data_model_view.set_variable_lower_bounds(variable_lower_bounds.data(), + variable_lower_bounds.size()); + data_model_view.set_variable_upper_bounds(variable_upper_bounds.data(), + variable_upper_bounds.size()); + } + + // Set row types (constraint types) + if (get_row_types().size() != 0) { + data_model_view.set_row_types(row_types.data(), row_types.size()); + } + + // Set constraint bounds (lower and upper) + if (get_constraint_lower_bounds().size() != 0 && get_constraint_upper_bounds().size() != 0) { + data_model_view.set_constraint_lower_bounds(constraint_lower_bounds.data(), + constraint_lower_bounds.size()); + data_model_view.set_constraint_upper_bounds(constraint_upper_bounds.data(), + constraint_upper_bounds.size()); + } + + // Create a temporary vector to hold the converted variable types + std::vector variable_types(get_n_variables()); + // Set variable types (convert from enum to char) + if (get_n_variables() != 0) { + auto enum_variable_types = cuopt::host_copy(get_variable_types()); + + // 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'; + } + + data_model_view.set_variable_types(variable_types.data(), variable_types.size()); + } + + // Set problem and variable names if available + if (!get_problem_name().empty()) { data_model_view.set_problem_name(get_problem_name()); } + + if (!get_objective_name().empty()) { data_model_view.set_objective_name(get_objective_name()); } + + if (!get_variable_names().empty()) { data_model_view.set_variable_names(get_variable_names()); } + + if (!get_row_names().empty()) { data_model_view.set_row_names(get_row_names()); } + + cuopt::mps_parser::write_mps(data_model_view, mps_file_path); +} + // NOTE: Explicitly instantiate all types here in order to avoid linker error #if MIP_INSTANTIATE_FLOAT template class optimization_problem_t; diff --git a/cpp/src/linear_programming/solve.cu b/cpp/src/linear_programming/solve.cu index 95a62df2a..156dd5296 100644 --- a/cpp/src/linear_programming/solve.cu +++ b/cpp/src/linear_programming/solve.cu @@ -619,11 +619,6 @@ optimization_problem_solution_t solve_lp(optimization_problem_t solve_lp(optimization_problem_t #include #include +#include +#include +#include #include #include @@ -106,6 +109,14 @@ data_model_to_optimization_problem( op_problem.set_variable_types(enum_variable_types.data(), enum_variable_types.size()); } + if (data_model->get_variable_names().size() != 0) { + op_problem.set_variable_names(data_model->get_variable_names()); + } + + if (data_model->get_row_names().size() != 0) { + op_problem.set_row_names(data_model->get_row_names()); + } + return op_problem; } diff --git a/cpp/src/mip/CMakeLists.txt b/cpp/src/mip/CMakeLists.txt index 7165b3bb5..8e72ca70e 100644 --- a/cpp/src/mip/CMakeLists.txt +++ b/cpp/src/mip/CMakeLists.txt @@ -13,12 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. + # Files necessary for Linear Programming functionality set(MIP_LP_NECESSARY_FILES ${CMAKE_CURRENT_SOURCE_DIR}/problem/problem.cu ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu ${CMAKE_CURRENT_SOURCE_DIR}/solver_solution.cu - ${CMAKE_CURRENT_SOURCE_DIR}/problem/write_mps.cu ${CMAKE_CURRENT_SOURCE_DIR}/local_search/rounding/simple_rounding.cu ${CMAKE_CURRENT_SOURCE_DIR}/presolve/third_party_presolve.cpp ${CMAKE_CURRENT_SOURCE_DIR}/solution/solution.cu diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index b237caea9..0e5cf510c 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -140,6 +140,7 @@ problem_t::problem_t( var_names(problem_.get_variable_names()), row_names(problem_.get_row_names()), objective_name(problem_.get_objective_name()), + objective_offset(problem_.get_objective_offset()), lp_state(*this, problem_.get_handle_ptr()->get_stream()), fixing_helpers(n_constraints, n_variables, handle_ptr) { diff --git a/cpp/src/mip/problem/problem.cuh b/cpp/src/mip/problem/problem.cuh index 9d63e1857..f9c147b89 100644 --- a/cpp/src/mip/problem/problem.cuh +++ b/cpp/src/mip/problem/problem.cuh @@ -112,7 +112,6 @@ class problem_t { void get_host_user_problem( cuopt::linear_programming::dual_simplex::user_problem_t& user_problem) const; - void write_as_mps(const std::string& path); void add_cutting_plane_at_objective(f_t objective); void compute_vars_with_objective_coeffs(); @@ -266,6 +265,7 @@ class problem_t { std::vector row_names{}; /** name of the objective (only a single objective is currently allowed) */ std::string objective_name; + f_t objective_offset; bool is_scaled_{false}; bool preprocess_called{false}; // this LP state keeps the warm start data of some solution of diff --git a/cpp/src/mip/solve.cu b/cpp/src/mip/solve.cu index 486544eba..09da4b376 100644 --- a/cpp/src/mip/solve.cu +++ b/cpp/src/mip/solve.cu @@ -214,7 +214,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, } if (settings.user_problem_file != "") { CUOPT_LOG_INFO("Writing user problem to file: %s", settings.user_problem_file.c_str()); - problem.write_as_mps(settings.user_problem_file); + op_problem.write_to_mps(settings.user_problem_file); } // this is for PDLP, i think this should be part of pdlp solver diff --git a/cpp/tests/mip/doc_example_test.cu b/cpp/tests/mip/doc_example_test.cu index 3506eb00d..e154e8c69 100644 --- a/cpp/tests/mip/doc_example_test.cu +++ b/cpp/tests/mip/doc_example_test.cu @@ -160,12 +160,16 @@ TEST(docs, user_problem_file) // Get solution values const auto& sol_values = solution.get_solution(); // x should be approximately 37 and integer - EXPECT_NEAR(37.0, sol_values.element(0, handle_.get_stream()), 0.1); - EXPECT_NEAR(std::round(sol_values.element(0, handle_.get_stream())), - sol_values.element(0, handle_.get_stream()), - settings.tolerances.integrality_tolerance); // Check x is integer - // y should be approximately 39.5 - EXPECT_NEAR(39.5, sol_values.element(1, handle_.get_stream()), 0.1); + for (int i = 0; i < problem2.get_n_variables(); i++) { + if (problem2.get_variable_names()[i] == "x") { + EXPECT_NEAR(37.0, sol_values.element(i, handle_.get_stream()), 0.1); + EXPECT_NEAR(std::round(sol_values.element(i, handle_.get_stream())), + sol_values.element(i, handle_.get_stream()), + settings.tolerances.integrality_tolerance); // Check x is integer + } else { // y should be approximately 39.5 + EXPECT_NEAR(39.5, sol_values.element(i, handle_.get_stream()), 0.1); + } + } } } // namespace cuopt::linear_programming::test diff --git a/docs/cuopt/source/conf.py b/docs/cuopt/source/conf.py index 517a57da9..765ec1c96 100644 --- a/docs/cuopt/source/conf.py +++ b/docs/cuopt/source/conf.py @@ -300,6 +300,7 @@ ("py:obj", "cuopt_sh_client.PDLPSolverMode.is_integer"), ("py:obj", "cuopt_sh_client.PDLPSolverMode.bit_count"), ("py:obj", "cuopt_sh_client.PDLPSolverMode.bit_length"), + ("py:obj", "data_model.DataModel.set_data_model_view"), ("c:type", "size_t"), ("c:identifier", "int32_t"), ("c:identifier", "int8_t"), diff --git a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd index 3ab51cd7a..7a9950387 100644 --- a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd @@ -44,6 +44,8 @@ cdef extern from "mps_parser/mps_data_model.hpp" namespace "cuopt::mps_parser": vector[string] var_names_ vector[string] row_names_ vector[char] row_types_ + string objective_name_ + string problem_name_ cdef extern from "mps_parser/utilities/cython_mps_parser.hpp" namespace "cuopt::cython": # noqa diff --git a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx index 9b150a7eb..819c12ceb 100644 --- a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx @@ -129,5 +129,7 @@ def ParseMps(mps_file_path, fixed_mps_formats): data_model.set_row_types(row_types) data_model.set_variable_names(var_names_) data_model.set_row_names(row_names_) + data_model.set_objective_name(dm_ret.objective_name_.decode()) + data_model.set_problem_name(dm_ret.problem_name_.decode()) return data_model diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd index 8f5911bf0..247d1c53b 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd @@ -20,6 +20,8 @@ # cython: language_level = 3 from libcpp cimport bool +from libcpp.string cimport string +from libcpp.vector cimport vector cdef extern from "mps_parser/data_model_view.hpp" namespace "cuopt::mps_parser" nogil: # noqa @@ -56,3 +58,14 @@ cdef extern from "mps_parser/data_model_view.hpp" namespace "cuopt::mps_parser" i_t size) except + void set_row_types(const char* row_types, i_t size) except + void set_variable_types(const char* var_types, i_t size) except + + void set_variable_names(const vector[string] variables_names) except + + void set_row_names(const vector[string] row_names) except + + void set_problem_name(const string problem_name) except + + void set_objective_name(const string objective_name) except + + + +cdef extern from "mps_parser/writer.hpp" namespace "cuopt::mps_parser" nogil: # noqa + + cdef void write_mps( + const data_model_view_t[int, double] data_model, + const string user_problem_file) except + diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index 0301b3603..d5e43302d 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -411,6 +411,20 @@ def set_row_names(self, row_names): """ super().set_row_names(row_names) + @catch_cuopt_exception + def set_objective_name(self, objective_name): + """ + Set the objective name as string. + """ + super().set_objective_name(objective_name) + + @catch_cuopt_exception + def set_problem_name(self, problem_name): + """ + Set the problem name as string. + """ + super().set_problem_name(problem_name) + @catch_cuopt_exception def set_initial_primal_solution(self, initial_primal_solution): """ @@ -603,3 +617,21 @@ def get_row_names(self): """ return super().get_row_names() + + @catch_cuopt_exception + def get_objective_name(self): + """ + Get the objective name as string. + """ + return super().get_objective_name() + + @catch_cuopt_exception + def get_problem_name(self): + """ + Get the problem name as string. + """ + return super().get_problem_name() + + @catch_cuopt_exception + def writeMPS(self, user_problem_file): + return super().writeMPS(user_problem_file) diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index 11f3308bb..50641d331 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -19,12 +19,15 @@ # cython: embedsignature = True # cython: language_level = 3 -from .data_model cimport data_model_view_t +from .data_model cimport data_model_view_t, write_mps import warnings import numpy as np +import cudf + +from libc.stdint cimport uintptr_t from libcpp.memory cimport unique_ptr from libcpp.utility cimport move @@ -42,6 +45,17 @@ def type_cast(np_obj, np_type, name): return np_obj +def get_data_ptr(array): + if isinstance(array, cudf.Series): + return array.__cuda_array_interface__['data'][0] + elif isinstance(array, np.ndarray): + return array.__array_interface__['data'][0] + else: + raise Exception( + "get_data_ptr must be called with cudf.Series or np.ndarray" + ) + + cdef class DataModel: def __init__(self): @@ -133,6 +147,12 @@ cdef class DataModel: def set_row_names(self, row_names): self.row_names = row_names + def set_objective_name(self, objective_name): + self.objective_name = objective_name + + def set_problem_name(self, problem_name): + self.problem_name = problem_name + def get_sense(self): return self.maximize @@ -189,3 +209,165 @@ cdef class DataModel: def get_row_names(self): return self.row_names + + def get_objective_name(self): + return self.objective_name + + def get_problem_name(self): + return self.problem_name + + def set_data_model_view(self): + cdef data_model_view_t[int, double]* c_data_model_view = ( + self.c_data_model_view.get() + ) + + # Set self.fields on the C++ side if set on the Python side + cdef uintptr_t c_A_values = ( + get_data_ptr(self.get_constraint_matrix_values()) + ) + cdef uintptr_t c_A_indices = ( + get_data_ptr(self.get_constraint_matrix_indices()) + ) + cdef uintptr_t c_A_offsets = ( + get_data_ptr(self.get_constraint_matrix_offsets()) + ) + if self.get_constraint_matrix_values().shape[0] != 0 and self.get_constraint_matrix_indices().shape[0] != 0 and self.get_constraint_matrix_offsets().shape[0] != 0: # noqa + c_data_model_view.set_csr_constraint_matrix( + c_A_values, + self.get_constraint_matrix_values().shape[0], + c_A_indices, + self.get_constraint_matrix_indices().shape[0], + c_A_offsets, + self.get_constraint_matrix_offsets().shape[0] + ) + + cdef uintptr_t c_b = ( + get_data_ptr(self.get_constraint_bounds()) + ) + if self.get_constraint_bounds().shape[0] != 0: + c_data_model_view.set_constraint_bounds( + c_b, + self.get_constraint_bounds().shape[0] + ) + + cdef uintptr_t c_c = ( + get_data_ptr(self.get_objective_coefficients()) + ) + if self.get_objective_coefficients().shape[0] != 0: + c_data_model_view.set_objective_coefficients( + c_c, + self.get_objective_coefficients().shape[0] + ) + + c_data_model_view.set_objective_scaling_factor( + self.get_objective_scaling_factor() + ) + c_data_model_view.set_objective_offset( + self.get_objective_offset() + ) + c_data_model_view.set_maximize( self.maximize) + + cdef uintptr_t c_variable_lower_bounds = ( + get_data_ptr(self.get_variable_lower_bounds()) + ) + if self.get_variable_lower_bounds().shape[0] != 0: + c_data_model_view.set_variable_lower_bounds( + c_variable_lower_bounds, + self.get_variable_lower_bounds().shape[0] + ) + + cdef uintptr_t c_variable_upper_bounds = ( + get_data_ptr(self.get_variable_upper_bounds()) + ) + if self.get_variable_upper_bounds().shape[0] != 0: + c_data_model_view.set_variable_upper_bounds( + c_variable_upper_bounds, + self.get_variable_upper_bounds().shape[0] + ) + cdef uintptr_t c_constraint_lower_bounds = ( + get_data_ptr(self.get_constraint_lower_bounds()) + ) + if self.get_constraint_lower_bounds().shape[0] != 0: + c_data_model_view.set_constraint_lower_bounds( + c_constraint_lower_bounds, + self.get_constraint_lower_bounds().shape[0] + ) + cdef uintptr_t c_constraint_upper_bounds = ( + get_data_ptr(self.get_constraint_upper_bounds()) + ) + if self.get_constraint_upper_bounds().shape[0] != 0: + c_data_model_view.set_constraint_upper_bounds( + c_constraint_upper_bounds, + self.get_constraint_upper_bounds().shape[0] + ) + cdef uintptr_t c_row_types = ( + get_data_ptr(self.get_ascii_row_types()) + ) + if self.get_ascii_row_types().shape[0] != 0: + c_data_model_view.set_row_types( + c_row_types, + self.get_ascii_row_types().shape[0] + ) + + cdef uintptr_t c_var_types = ( + get_data_ptr(self.get_variable_types()) + ) + if self.get_variable_types().shape[0] != 0: + c_data_model_view.set_variable_types( + c_var_types, + self.get_variable_types().shape[0] + ) + + cdef vector[string] c_var_names + for s in self.get_variable_names(): + c_var_names.push_back(s.encode()) + + if len(self.get_variable_names()) != 0: + c_data_model_view.set_variable_names( + c_var_names + ) + + cdef vector[string] c_row_names + for s in self.get_row_names(): + c_row_names.push_back(s.encode()) + + if len(self.get_row_names()) != 0: + c_data_model_view.set_row_names( + c_row_names + ) + + if self.get_problem_name(): + c_data_model_view.set_problem_name( + self.get_problem_name().encode() + ) + + if self.get_objective_name(): + c_data_model_view.set_objective_name( + self.get_objective_name().encode() + ) + + # Set initial solution on the C++ side if set on the Python side + cdef uintptr_t c_initial_primal_solution = ( + get_data_ptr(self.get_initial_primal_solution()) + ) + if self.get_initial_primal_solution().shape[0] != 0: + c_data_model_view.set_initial_primal_solution( + c_initial_primal_solution, + self.get_initial_primal_solution().shape[0] + ) + cdef uintptr_t c_initial_dual_solution = ( + get_data_ptr(self.get_initial_dual_solution()) + ) + if self.get_initial_dual_solution().shape[0] != 0: + c_data_model_view.set_initial_dual_solution( + c_initial_dual_solution, + self.get_initial_dual_solution().shape[0] + ) + + def writeMPS(self, user_problem_file): + self.variable_types = type_cast( + self.variable_types, "S1", "variable_types" + ) + self.set_data_model_view() + write_mps(self.c_data_model_view.get()[0], + user_problem_file.encode('utf-8')) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 1a14e17cf..9ad77ca58 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -13,8 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy from enum import Enum +import cuopt_mps_parser import numpy as np import cuopt.linear_programming.data_model as data_model @@ -699,12 +701,11 @@ def __init__(self, model_name=""): self.Status = -1 self.ObjValue = float("nan") + self.model = None self.solved = False self.rhs = None self.row_sense = None - self.row_pointers = None - self.column_indicies = None - self.values = None + self.constraint_csr_matrix = None self.lower_bound = None self.upper_bound = None self.var_type = None @@ -714,6 +715,124 @@ def __init__(self, mdict): for key, value in mdict.items(): setattr(self, key, value) + def _from_data_model(self, dm): + self.Name = dm.get_problem_name() + obj_coeffs = dm.get_objective_coefficients() + obj_constant = dm.get_objective_offset() + num_vars = len(obj_coeffs) + sense = dm.get_sense() + if sense: + sense = MAXIMIZE + else: + sense = MINIMIZE + v_lb = dm.get_variable_lower_bounds() + v_ub = dm.get_variable_upper_bounds() + v_types = dm.get_variable_types() + v_names = dm.get_variable_names().tolist() + + # Add all Variables and Objective Coefficients + for i in range(num_vars): + v_name = "" + if v_names: + v_name = v_names[i] + self.addVariable(v_lb[i], v_ub[i], vtype=v_types[i], name=v_name) + vars = self.getVariables() + expr = LinearExpression(vars, obj_coeffs, obj_constant) + self.setObjective(expr, sense) + + # Add all Constraints + c_lb = dm.get_constraint_lower_bounds() + c_ub = dm.get_constraint_upper_bounds() + c_b = dm.get_constraint_bounds() + c_names = dm.get_row_names() + offsets = dm.get_constraint_matrix_offsets() + indices = dm.get_constraint_matrix_indices() + values = dm.get_constraint_matrix_values() + + num_constrs = len(offsets) - 1 + for i in range(num_constrs): + start = offsets[i] + end = offsets[i + 1] + c_coeffs = values[start:end] + c_indices = indices[start:end] + c_vars = [vars[j] for j in c_indices] + expr = LinearExpression(c_vars, c_coeffs, 0.0) + if c_lb[i] == c_ub[i]: + self.addConstraint(expr == c_b[i], name=c_names[i]) + elif c_lb[i] == c_b[i]: + self.addConstraint(expr >= c_b[i], name=c_names[i]) + elif c_ub[i] == c_b[i]: + self.addConstraint(expr <= c_b[i], name=c_names[i]) + else: + raise Exception("Couldn't initialize constraints") + + def _to_data_model(self): + # iterate through the constraints and construct the constraint matrix + n = len(self.vars) + self.rhs = [] + self.row_sense = [] + self.row_names = [] + + if self.constraint_csr_matrix is None: + csr_dict = { + "row_pointers": [0], + "column_indices": [], + "values": [], + } + for constr in self.constrs: + csr_dict["column_indices"].extend( + list(constr.vindex_coeff_dict.keys()) + ) + csr_dict["values"].extend( + list(constr.vindex_coeff_dict.values()) + ) + csr_dict["row_pointers"].append( + len(csr_dict["column_indices"]) + ) + self.rhs.append(constr.RHS) + self.row_sense.append(constr.Sense) + self.row_names.append(constr.ConstraintName) + self.constraint_csr_matrix = csr_dict + + else: + for constr in self.constrs: + self.rhs.append(constr.RHS) + self.row_sense.append(constr.Sense) + + self.objective = np.zeros(n) + self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) + self.var_type = np.empty(n, dtype="S1") + self.var_names = [] + + for j in range(n): + self.objective[j] = self.vars[j].getObjectiveCoefficient() + self.var_type[j] = self.vars[j].getVariableType() + self.lower_bound[j] = self.vars[j].getLowerBound() + self.upper_bound[j] = self.vars[j].getUpperBound() + self.var_names.append(self.vars[j].VariableName) + + # Initialize datamodel + dm = data_model.DataModel() + dm.set_csr_constraint_matrix( + np.array(self.constraint_csr_matrix["values"]), + np.array(self.constraint_csr_matrix["column_indices"]), + np.array(self.constraint_csr_matrix["row_pointers"]), + ) + if self.ObjSense == -1: + dm.set_maximize(True) + dm.set_constraint_bounds(np.array(self.rhs)) + dm.set_row_types(np.array(self.row_sense, dtype="S1")) + dm.set_objective_coefficients(self.objective) + dm.set_objective_offset(self.ObjConstant) + dm.set_variable_lower_bounds(self.lower_bound) + dm.set_variable_upper_bounds(self.upper_bound) + dm.set_variable_types(self.var_type) + dm.set_variable_names(self.var_names) + dm.set_row_names(self.row_names) + dm.set_problem_name(self.Name) + + self.model = dm + def reset_solved_values(self): # Resets all post solve values for var in self.vars: @@ -724,6 +843,8 @@ def reset_solved_values(self): constr.Slack = float("nan") constr.DualValue = float("nan") + self.model = None + self.constraint_csr_matrix = None self.ObjValue = float("nan") self.solved = False @@ -823,7 +944,7 @@ def setObjective(self, expr, sense=MINIMIZE): case int() | float(): for var in self.vars: var.setObjectiveCoefficient(0.0) - self.ObjCon = float(expr) + self.ObjConstant = float(expr) case Variable(): for var in self.vars: var.setObjectiveCoefficient(0.0) @@ -832,6 +953,7 @@ def setObjective(self, expr, sense=MINIMIZE): case LinearExpression(): for var, coeff in expr.zipVarCoefficients(): self.vars[var.getIndex()].setObjectiveCoefficient(coeff) + self.ObjConstant = expr.getConstant() case _: raise ValueError( "Objective must be a LinearExpression or a constant" @@ -856,6 +978,22 @@ def getConstraints(self): """ return self.constrs + @classmethod + def readMPS(cls, mps_file): + """ + Initiliaze a problem from an MPS file. + """ + problem = cls() + data_model = cuopt_mps_parser.ParseMps(mps_file) + problem._from_data_model(data_model) + problem.model = data_model + return problem + + def writeMPS(self, mps_file): + if self.model is None: + self._to_data_model() + self.model.writeMPS(mps_file) + @property def NumVariables(self): # Returns number of variables in the problem @@ -887,6 +1025,8 @@ def getCSR(self): Computes and returns the CSR representation of the constraint matrix. """ + if self.constraint_csr_matrix is not None: + return self.dict_to_object(self.constraint_csr_matrix) csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} for constr in self.constrs: csr_dict["column_indices"].extend( @@ -894,6 +1034,7 @@ def getCSR(self): ) csr_dict["values"].extend(list(constr.vindex_coeff_dict.values())) csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) + self.constraint_csr_matrix = csr_dict return self.dict_to_object(csr_dict) def get_incumbent_values(self, solution, vars): @@ -905,7 +1046,18 @@ def get_incumbent_values(self, solution, vars): values.append(solution[var.index]) return values - def post_solve(self, solution): + def relax(self): + """ + Relax a MIP problem into an LP problem and return the relaxed model. + """ + self.reset_solved_values() + relaxed_problem = copy.deepcopy(self) + vars = relaxed_problem.getVariables() + for v in vars: + v.VariableType = CONTINUOUS + return relaxed_problem + + def populate_solution(self, solution): self.Status = solution.get_termination_status() self.SolveTime = solution.get_solve_time() @@ -950,48 +1102,11 @@ def solve(self, settings=solver_settings.SolverSettings()): >>> problem.solve() """ - # iterate through the constraints and construct the constraint matrix - n = len(self.vars) - self.row_pointers = [0] - self.column_indicies = [] - self.values = [] - self.rhs = [] - self.row_sense = [] - for constr in self.constrs: - self.column_indicies.extend(list(constr.vindex_coeff_dict.keys())) - self.values.extend(list(constr.vindex_coeff_dict.values())) - self.row_pointers.append(len(self.column_indicies)) - self.rhs.append(constr.RHS) - self.row_sense.append(constr.Sense) - - self.objective = np.zeros(n) - self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) - self.var_type = np.empty(n, dtype="S1") - - for j in range(n): - self.objective[j] = self.vars[j].getObjectiveCoefficient() - self.var_type[j] = self.vars[j].getVariableType() - self.lower_bound[j] = self.vars[j].getLowerBound() - self.upper_bound[j] = self.vars[j].getUpperBound() - - # Initialize datamodel - dm = data_model.DataModel() - dm.set_csr_constraint_matrix( - np.array(self.values), - np.array(self.column_indicies), - np.array(self.row_pointers), - ) - if self.ObjSense == -1: - dm.set_maximize(True) - dm.set_constraint_bounds(np.array(self.rhs)) - dm.set_row_types(np.array(self.row_sense, dtype="S1")) - dm.set_objective_coefficients(self.objective) - dm.set_variable_lower_bounds(self.lower_bound) - dm.set_variable_upper_bounds(self.upper_bound) - dm.set_variable_types(self.var_type) + if self.model is None: + self._to_data_model() # Call Solver - solution = solver.Solve(dm, settings) + solution = solver.Solve(self.model, settings) # Post Solve - self.post_solve(solution) + self.populate_solution(solution) diff --git a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx index 02782b8f9..a468d57ae 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx @@ -154,127 +154,6 @@ def type_cast(cudf_obj, np_type, name): return cudf_obj -cdef set_data_model_view(DataModel data_model_obj): - cdef data_model_view_t[int, double]* c_data_model_view = ( - data_model_obj.c_data_model_view.get() - ) - - # Set data_model_obj fields on the C++ side if set on the Python side - cdef uintptr_t c_A_values = ( - get_data_ptr(data_model_obj.get_constraint_matrix_values()) - ) - cdef uintptr_t c_A_indices = ( - get_data_ptr(data_model_obj.get_constraint_matrix_indices()) - ) - cdef uintptr_t c_A_offsets = ( - get_data_ptr(data_model_obj.get_constraint_matrix_offsets()) - ) - if data_model_obj.get_constraint_matrix_values().shape[0] != 0 and data_model_obj.get_constraint_matrix_indices().shape[0] != 0 and data_model_obj.get_constraint_matrix_offsets().shape[0] != 0: # noqa - c_data_model_view.set_csr_constraint_matrix( - c_A_values, - data_model_obj.get_constraint_matrix_values().shape[0], - c_A_indices, - data_model_obj.get_constraint_matrix_indices().shape[0], - c_A_offsets, - data_model_obj.get_constraint_matrix_offsets().shape[0] - ) - - cdef uintptr_t c_b = ( - get_data_ptr(data_model_obj.get_constraint_bounds()) - ) - if data_model_obj.get_constraint_bounds().shape[0] != 0: - c_data_model_view.set_constraint_bounds( - c_b, - data_model_obj.get_constraint_bounds().shape[0] - ) - - cdef uintptr_t c_c = ( - get_data_ptr(data_model_obj.get_objective_coefficients()) - ) - if data_model_obj.get_objective_coefficients().shape[0] != 0: - c_data_model_view.set_objective_coefficients( - c_c, - data_model_obj.get_objective_coefficients().shape[0] - ) - - c_data_model_view.set_objective_scaling_factor( - data_model_obj.get_objective_scaling_factor() - ) - c_data_model_view.set_objective_offset( - data_model_obj.get_objective_offset() - ) - c_data_model_view.set_maximize( data_model_obj.maximize) - - cdef uintptr_t c_variable_lower_bounds = ( - get_data_ptr(data_model_obj.get_variable_lower_bounds()) - ) - if data_model_obj.get_variable_lower_bounds().shape[0] != 0: - c_data_model_view.set_variable_lower_bounds( - c_variable_lower_bounds, - data_model_obj.get_variable_lower_bounds().shape[0] - ) - - cdef uintptr_t c_variable_upper_bounds = ( - get_data_ptr(data_model_obj.get_variable_upper_bounds()) - ) - if data_model_obj.get_variable_upper_bounds().shape[0] != 0: - c_data_model_view.set_variable_upper_bounds( - c_variable_upper_bounds, - data_model_obj.get_variable_upper_bounds().shape[0] - ) - cdef uintptr_t c_constraint_lower_bounds = ( - get_data_ptr(data_model_obj.get_constraint_lower_bounds()) - ) - if data_model_obj.get_constraint_lower_bounds().shape[0] != 0: - c_data_model_view.set_constraint_lower_bounds( - c_constraint_lower_bounds, - data_model_obj.get_constraint_lower_bounds().shape[0] - ) - cdef uintptr_t c_constraint_upper_bounds = ( - get_data_ptr(data_model_obj.get_constraint_upper_bounds()) - ) - if data_model_obj.get_constraint_upper_bounds().shape[0] != 0: - c_data_model_view.set_constraint_upper_bounds( - c_constraint_upper_bounds, - data_model_obj.get_constraint_upper_bounds().shape[0] - ) - cdef uintptr_t c_row_types = ( - get_data_ptr(data_model_obj.get_ascii_row_types()) - ) - if data_model_obj.get_ascii_row_types().shape[0] != 0: - c_data_model_view.set_row_types( - c_row_types, - data_model_obj.get_ascii_row_types().shape[0] - ) - - cdef uintptr_t c_var_types = ( - get_data_ptr(data_model_obj.get_variable_types()) - ) - if data_model_obj.get_variable_types().shape[0] != 0: - c_data_model_view.set_variable_types( - c_var_types, - data_model_obj.get_variable_types().shape[0] - ) - - # Set initial solution on the C++ side if set on the Python side - cdef uintptr_t c_initial_primal_solution = ( - get_data_ptr(data_model_obj.get_initial_primal_solution()) - ) - if data_model_obj.get_initial_primal_solution().shape[0] != 0: - c_data_model_view.set_initial_primal_solution( - c_initial_primal_solution, - data_model_obj.get_initial_primal_solution().shape[0] - ) - cdef uintptr_t c_initial_dual_solution = ( - get_data_ptr(data_model_obj.get_initial_dual_solution()) - ) - if data_model_obj.get_initial_dual_solution().shape[0] != 0: - c_data_model_view.set_initial_dual_solution( - c_initial_dual_solution, - data_model_obj.get_initial_dual_solution().shape[0] - ) - - cdef set_solver_setting( unique_ptr[solver_settings_t[int, double]]& unique_solver_settings, settings, @@ -675,7 +554,7 @@ def Solve(py_data_model_obj, settings, mip=False): set_solver_setting( unique_solver_settings, settings, data_model_obj, mip ) - set_data_model_view(data_model_obj) + data_model_obj.set_data_model_view() return create_solution(move(call_solve( data_model_obj.c_data_model_view.get(), @@ -683,8 +562,10 @@ def Solve(py_data_model_obj, settings, mip=False): )), data_model_obj) -cdef insert_vector(DataModel data_model_obj, - vector[data_model_view_t[int, double] *]& data_model_views): +cdef set_and_insert_vector( + DataModel data_model_obj, + vector[data_model_view_t[int, double] *]& data_model_views): + data_model_obj.set_data_model_view() data_model_views.push_back(data_model_obj.c_data_model_view.get()) @@ -699,8 +580,7 @@ def BatchSolve(py_data_model_list, settings): cdef vector[data_model_view_t[int, double] *] data_model_views for data_model_obj in py_data_model_list: - set_data_model_view(data_model_obj) - insert_vector(data_model_obj, data_model_views) + set_and_insert_vector(data_model_obj, data_model_views) cdef pair[ vector[unique_ptr[solver_ret_t]], 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 132920a86..1cc4993d2 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -26,6 +26,7 @@ CONTINUOUS, INTEGER, MAXIMIZE, + MINIMIZE, CType, Problem, VType, @@ -265,6 +266,53 @@ def test_constraint_matrix(): assert rhs == exp_rhs +def test_read_write_mps_and_relaxation(): + + # Create MIP model + m = Problem("SMALLMIP") + + # Vars: continuous, nonnegative by default + x1 = m.addVariable(name="x1", lb=0.0, vtype=INTEGER) + x2 = m.addVariable(name="x2", lb=0.0, ub=4.0, vtype=INTEGER) + x3 = m.addVariable(name="x3", lb=0.0, ub=6.0, vtype=INTEGER) + x4 = m.addVariable(name="x4", lb=0.0, vtype=INTEGER) + x5 = m.addVariable(name="x5", lb=0.0, vtype=INTEGER) + + # Objective (minimize) + m.setObjective(2 * x1 + 3 * x2 + x3 + 1 * x4 + 4 * x5, MINIMIZE) + + # Constraints (5 total) + m.addConstraint(x1 + x2 + x3 <= 10, name="c1") + m.addConstraint(2 * x1 + x3 - x4 >= 3, name="c2") + m.addConstraint(x2 + 3 * x5 == 7, name="c3") + m.addConstraint(x4 + x5 <= 8, name="c4") + m.addConstraint(x1 + x2 + x3 + x4 + x5 >= 5, name="c5") + + # Write MPS + m.writeMPS("small_mip.mps") + + # Read MPS and solve + prob = Problem.readMPS("small_mip.mps") + assert prob.Name == "SMALLMIP" + assert prob.IsMIP + prob.solve() + + expected_values_mip = [1.0, 1.0, 1.0, 0.0, 2.0] + assert prob.Status.name == "Optimal" + for i, v in enumerate(prob.getVariables()): + assert v.getValue() == pytest.approx(expected_values_mip[i]) + + # Relax the Problem into LP and solve + lp_prob = prob.relax() + assert not lp_prob.IsMIP + lp_prob.solve() + + expected_values_lp = [0.33333333, 0.0, 2.33333333, 0.0, 2.33333333] + assert lp_prob.Status.name == "Optimal" + for i, v in enumerate(lp_prob.getVariables()): + assert v.getValue() == pytest.approx(expected_values_lp[i]) + + def test_incumbent_solutions(): # Callback for incumbent solution