diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index f6be07aaa9..a425f6d70b 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -225,4 +225,36 @@ #define CUOPT_BARRIER_ITERATIVE_REFINEMENT_OFF 0 #define CUOPT_BARRIER_ITERATIVE_REFINEMENT_ON 1 +/* @brief Scalar problem attribute selectors + * Passed as cuopt_int_t; the valid set depends on the accessor's value type. */ +#define CUOPT_ATTR_NUM_VARIABLES 0 +#define CUOPT_ATTR_NUM_CONSTRAINTS 1 +#define CUOPT_ATTR_NUM_NONZEROS 2 +#define CUOPT_ATTR_NUM_INTEGERS 3 +#define CUOPT_ATTR_OBJECTIVE_SENSE 4 +#define CUOPT_ATTR_OBJECTIVE_OFFSET 5 +#define CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR 6 +#define CUOPT_ATTR_PROBLEM_CATEGORY 7 +#define CUOPT_ATTR_IS_MIP 8 +#define CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE 9 +#define CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS 10 + +/* @brief Numeric/char array problem attribute selectors + * (see cuOptGetProblem{Float,Char}ArrayAttribute; sized by num_variables / num_constraints). + * Passed as cuopt_int_t. Numbered in a separate range from the scalar selectors for safety. + */ +#define CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS 100 +#define CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS 101 +#define CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS 102 +#define CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS 103 +#define CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS 104 +#define CUOPT_ARRAY_ATTR_CONSTRAINT_RHS 105 +#define CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE 106 +#define CUOPT_ARRAY_ATTR_VARIABLE_TYPES 107 + +/* @brief String-array problem attribute selectors (see cuOptGetProblemStringArrayAttribute). + * Passed as cuopt_int_t; numbered in a separate range from the scalar and array selectors. */ +#define CUOPT_STRING_ARRAY_VARIABLE_NAMES 200 +#define CUOPT_STRING_ARRAY_ROW_NAMES 201 + #endif // CUOPT_CONSTANTS_H diff --git a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h index 218402a7ef..b135fed72a 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -563,7 +563,10 @@ cuopt_int_t cuOptGetObjectiveCoefficients(cuOptOptimizationProblem problem, */ cuopt_int_t cuOptGetNumNonZeros(cuOptOptimizationProblem problem, cuopt_int_t* num_non_zeros_ptr); -/** @brief Get the constraint matrix of an optimization problem in compressed sparse row format. +/** @brief Get the linear constraint matrix of an optimization problem in compressed sparse row + * format. This is the matrix of the linear constraints only. + * + * @note **Deprecated:** Use ``cuOptGetConstraintMatrixCSR``. * * @param[in] problem - The optimization problem. * @@ -586,6 +589,53 @@ cuopt_int_t cuOptGetConstraintMatrix(cuOptOptimizationProblem problem, cuopt_int_t* constraint_matrix_column_indices_ptr, cuopt_float_t* constraint_matrix_coefficients_ptr); +/** @brief Get the linear constraint matrix of an optimization problem in compressed sparse row + * format. This is the matrix of the linear constraints only. + * + * @param[in] problem - The optimization problem. + * + * @param[out] constraint_matrix_row_offsets_ptr - A pointer to an array of type cuopt_int_t of size + * num_constraints + 1 that on output will contain the row offsets of the constraint matrix. + * + * @param[out] constraint_matrix_column_indices_ptr - A pointer to an array of type cuopt_int_t of + * size equal to the number of nonzeros that on output will contain the column indices of the + * non-zero entries of the constraint matrix. + * + * @param[out] constraint_matrix_coefficients_ptr - A pointer to an array of type cuopt_float_t of + * size equal to the number of nonzeros that on output will contain the coefficients of the + * non-zero entries of the constraint matrix. + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptGetConstraintMatrixCSR(cuOptOptimizationProblem problem, + cuopt_int_t* constraint_matrix_row_offsets_ptr, + cuopt_int_t* constraint_matrix_column_indices_ptr, + cuopt_float_t* constraint_matrix_coefficients_ptr); + +/** @brief Get the linear constraint matrix of an optimization problem in compressed sparse column + * format. This is the matrix of the linear constraints only. + * + * @param[in] problem - The optimization problem. + * + * @param[out] constraint_matrix_column_offsets_ptr - A pointer to an array of type cuopt_int_t of + * size num_variables + 1 (see cuOptGetProblemIntAttribute) that on output will contain the column + * offsets of the constraint matrix. + * + * @param[out] constraint_matrix_row_indices_ptr - A pointer to an array of type cuopt_int_t of size + * equal to the number of nonzeros (see cuOptGetNumNonZeros) that on output will contain the row + * indices of the non-zero entries of the constraint matrix. + * + * @param[out] constraint_matrix_coefficients_ptr - A pointer to an array of type cuopt_float_t of + * size equal to the number of nonzeros that on output will contain the coefficients of the + * non-zero entries of the constraint matrix. + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptGetConstraintMatrixCSC(cuOptOptimizationProblem problem, + cuopt_int_t* constraint_matrix_column_offsets_ptr, + cuopt_int_t* constraint_matrix_row_indices_ptr, + cuopt_float_t* constraint_matrix_coefficients_ptr); + /** @brief Get the constraint sense of an optimization problem. * * @param[in] problem - The optimization problem. @@ -1055,6 +1105,64 @@ cuopt_int_t cuOptGetDualObjectiveValue(cuOptSolution solution, */ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_cost_ptr); +/* -------------------------------------------------------------------------- */ +/* Generic problem attributes */ +/* -------------------------------------------------------------------------- */ + +/* + * Attribute selectors are the CUOPT_ATTR_*, CUOPT_ARRAY_ATTR_*, and + * CUOPT_STRING_ARRAY_* integer constants defined in constants.h, passed as cuopt_int_t. + * + * These accessors use copy-out semantics: the caller allocates the output buffer and cuOpt copies + * values into it. Array attributes are sized by the problem dimensions: variable-indexed arrays + * have num_variables entries and constraint-indexed arrays have num_constraints entries (see + * cuOptGetProblemIntAttribute). The sole exception to copy-out is the + * string-array getter, which fills a caller-provided array of pointers with borrowed pointers into + * cuOpt-owned string storage; those pointers are valid until the problem is modified or destroyed + * and must not be freed. + * + * The constraint matrix is retrieved via cuOptGetConstraintMatrix (CSR) / + * cuOptGetConstraintMatrixCSC. + * + * TODO: there is no getter for the quadratic objective matrix (Q) + * or the quadratic constraint rows. + */ + +/** @brief Get a scalar integer problem attribute (a CUOPT_ATTR_* with an integer value). */ +cuopt_int_t cuOptGetProblemIntAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + cuopt_int_t* value_out); + +/** @brief Get a scalar floating-point problem attribute (objective offset / scaling factor). */ +cuopt_int_t cuOptGetProblemFloatAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + cuopt_float_t* value_out); + +/** @brief Copy a floating-point array attribute into out. count must equal num_variables for + * variable-indexed attributes or num_constraints for constraint-indexed attributes (see + * cuOptGetProblemIntAttribute). */ +cuopt_int_t cuOptGetProblemFloatArrayAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + cuopt_float_t* out, + cuopt_int_t count); + +/** @brief Copy a char array attribute (constraint sense or variable types) into out. count must + * equal num_constraints (constraint sense) or num_variables (variable types) (see + * cuOptGetProblemIntAttribute). */ +cuopt_int_t cuOptGetProblemCharArrayAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + char* out, + cuopt_int_t count); + +/** @brief Fill a caller-provided array of `count` pointers with borrowed pointers to cuOpt-owned + * strings (CUOPT_STRING_ARRAY_VARIABLE_NAMES or _ROW_NAMES). count must equal + * num_variables or num_constraints respectively. The returned pointers are valid until the problem + * is modified or destroyed; do not free them. */ +cuopt_int_t cuOptGetProblemStringArrayAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + const char** strings_out, + cuopt_int_t count); + #ifdef __cplusplus } #endif diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 5d987c1352..a813abf71f 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -21,6 +22,7 @@ #include +#include #include #include #include @@ -28,13 +30,17 @@ #include using cuopt::mathematical_optimization::char_to_var_type; +using cuopt::mathematical_optimization::csc_matrix_t; +using cuopt::mathematical_optimization::csr_matrix_t; using cuopt::mathematical_optimization::get_memory_backend_type; using cuopt::mathematical_optimization::is_valid_public_var_type_code; +using cuopt::mathematical_optimization::optimization_problem_interface_t; using cuopt::mathematical_optimization::problem_and_stream_view_t; using cuopt::mathematical_optimization::problem_category_t; using cuopt::mathematical_optimization::solution_and_stream_view_t; using cuopt::mathematical_optimization::solver_settings_t; using cuopt::mathematical_optimization::var_t; +using cuopt::mathematical_optimization::var_type_to_char; using cuopt::mathematical_optimization::io::mps_data_model_t; class c_get_solution_callback_t : public cuopt::internals::get_solution_callback_t { @@ -92,6 +98,76 @@ solver_settings_handle_t* get_settings_handle(cuOptSolverSettings settings) namespace { +// ---- Generic problem-attribute helpers (used by cuOptGetProblem*Attribute below) ---- + +problem_and_stream_view_t* as_problem(cuOptOptimizationProblem problem) +{ + return static_cast(problem); +} + +optimization_problem_interface_t* get_iface( + cuOptOptimizationProblem problem) +{ + return as_problem(problem)->get_problem(); +} + +bool is_int_attribute(cuopt_int_t attribute) +{ + switch (attribute) { + case CUOPT_ATTR_NUM_VARIABLES: + case CUOPT_ATTR_NUM_CONSTRAINTS: + case CUOPT_ATTR_NUM_NONZEROS: + case CUOPT_ATTR_NUM_INTEGERS: + case CUOPT_ATTR_OBJECTIVE_SENSE: + case CUOPT_ATTR_PROBLEM_CATEGORY: + case CUOPT_ATTR_IS_MIP: + case CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE: + case CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS: return true; + default: return false; + } +} + +bool is_float_attribute(cuopt_int_t attribute) +{ + return attribute == CUOPT_ATTR_OBJECTIVE_OFFSET || + attribute == CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR; +} + +bool is_float_array_attribute(cuopt_int_t attribute) +{ + switch (attribute) { + case CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS: + case CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS: + case CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS: + case CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS: + case CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS: + case CUOPT_ARRAY_ATTR_CONSTRAINT_RHS: return true; + default: return false; + } +} + +bool is_char_array_attribute(cuopt_int_t attribute) +{ + return attribute == CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE || + attribute == CUOPT_ARRAY_ATTR_VARIABLE_TYPES; +} + +cuopt_int_t get_array_size(optimization_problem_interface_t* problem, + cuopt_int_t attribute) +{ + switch (attribute) { + case CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS: + case CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS: + case CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS: + case CUOPT_ARRAY_ATTR_VARIABLE_TYPES: return problem->get_n_variables(); + case CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS: + case CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS: + case CUOPT_ARRAY_ATTR_CONSTRAINT_RHS: + case CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE: return problem->get_n_constraints(); + default: return -1; + } +} + void coo_to_csr(cuopt_int_t num_entries, const cuopt_int_t* row_index, const cuopt_int_t* col_index, @@ -169,6 +245,10 @@ constexpr char k_deprecated_quadratic_ranged_problem_msg[] = "linear problem, then cuOptSetQuadraticObjective to specify the quadratic objective terms. " "For QCQP models, call cuOptAddQuadraticConstraint for each quadratic constraint."; +constexpr char k_deprecated_get_constraint_matrix_msg[] = + "cuOptGetConstraintMatrix is deprecated. Use cuOptGetConstraintMatrixCSR for identical CSR " + "output, or cuOptGetConstraintMatrixCSC for compressed sparse column format."; + } // namespace int8_t cuOptGetFloatSize() { return sizeof(cuopt_float_t); } @@ -715,25 +795,11 @@ cuopt_int_t cuOptGetConstraintMatrix(cuOptOptimizationProblem problem, cuopt_int_t* constraint_matrix_column_indices_ptr, cuopt_float_t* constraint_matrix_coefficients_ptr) { - if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } - if (constraint_matrix_row_offsets_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } - if (constraint_matrix_column_indices_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } - if (constraint_matrix_coefficients_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } - problem_and_stream_view_t* problem_and_stream_view = - static_cast(problem); - - auto* prob = problem_and_stream_view->get_problem(); - cuopt_int_t num_nnz = prob->get_nnz(); - cuopt_int_t num_rows = prob->get_n_constraints(); - - prob->copy_constraint_matrix_to_host(constraint_matrix_coefficients_ptr, - constraint_matrix_column_indices_ptr, - constraint_matrix_row_offsets_ptr, - num_nnz, - num_nnz, - num_rows + 1); - - return CUOPT_SUCCESS; + CUOPT_LOG_ONCE(WARN, "%s", k_deprecated_get_constraint_matrix_msg); + return cuOptGetConstraintMatrixCSR(problem, + constraint_matrix_row_offsets_ptr, + constraint_matrix_column_indices_ptr, + constraint_matrix_coefficients_ptr); } cuopt_int_t cuOptGetConstraintSense(cuOptOptimizationProblem problem, char* constraint_sense_ptr) @@ -1295,3 +1361,211 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ return CUOPT_INVALID_ARGUMENT; } } + +/* -------------------------------------------------------------------------- */ +/* Generic problem attribute getters */ +/* -------------------------------------------------------------------------- */ + +cuopt_int_t cuOptGetProblemIntAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + cuopt_int_t* value_out) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (value_out == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (!is_int_attribute(attribute)) { return CUOPT_INVALID_ARGUMENT; } + + auto* iface = get_iface(problem); + switch (attribute) { + case CUOPT_ATTR_NUM_VARIABLES: *value_out = iface->get_n_variables(); return CUOPT_SUCCESS; + case CUOPT_ATTR_NUM_CONSTRAINTS: *value_out = iface->get_n_constraints(); return CUOPT_SUCCESS; + case CUOPT_ATTR_NUM_NONZEROS: *value_out = iface->get_nnz(); return CUOPT_SUCCESS; + case CUOPT_ATTR_NUM_INTEGERS: *value_out = iface->get_n_integers(); return CUOPT_SUCCESS; + case CUOPT_ATTR_OBJECTIVE_SENSE: + *value_out = iface->get_sense() ? CUOPT_MAXIMIZE : CUOPT_MINIMIZE; + return CUOPT_SUCCESS; + case CUOPT_ATTR_PROBLEM_CATEGORY: + *value_out = static_cast(iface->get_problem_category()); + return CUOPT_SUCCESS; + case CUOPT_ATTR_IS_MIP: { + const auto category = iface->get_problem_category(); + *value_out = + (category == problem_category_t::MIP || category == problem_category_t::IP) ? 1 : 0; + return CUOPT_SUCCESS; + } + case CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE: + *value_out = iface->has_quadratic_objective() ? 1 : 0; + return CUOPT_SUCCESS; + case CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS: + *value_out = iface->has_quadratic_constraints() ? 1 : 0; + return CUOPT_SUCCESS; + default: return CUOPT_INVALID_ARGUMENT; + } +} + +cuopt_int_t cuOptGetProblemFloatAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + cuopt_float_t* value_out) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (value_out == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (!is_float_attribute(attribute)) { return CUOPT_INVALID_ARGUMENT; } + + auto* iface = get_iface(problem); + if (attribute == CUOPT_ATTR_OBJECTIVE_OFFSET) { + *value_out = iface->get_objective_offset(); + } else { + *value_out = iface->get_objective_scaling_factor(); + } + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptGetProblemFloatArrayAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + cuopt_float_t* out, + cuopt_int_t count) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (out == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (!is_float_array_attribute(attribute)) { return CUOPT_INVALID_ARGUMENT; } + + auto* iface = get_iface(problem); + const cuopt_int_t expected = get_array_size(iface, attribute); + if (expected < 0 || count != expected) { return CUOPT_INVALID_ARGUMENT; } + + std::vector values; + switch (attribute) { + case CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS: + values = iface->get_objective_coefficients_host(); + break; + case CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS: + values = iface->get_variable_lower_bounds_host(); + break; + case CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS: + values = iface->get_variable_upper_bounds_host(); + break; + case CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS: + values = iface->get_constraint_lower_bounds_host(); + break; + case CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS: + values = iface->get_constraint_upper_bounds_host(); + break; + case CUOPT_ARRAY_ATTR_CONSTRAINT_RHS: values = iface->get_constraint_bounds_host(); break; + default: return CUOPT_INVALID_ARGUMENT; + } + + if (values.size() != expected) { return CUOPT_VALIDATION_ERROR; } + std::copy(values.begin(), values.end(), out); + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptGetProblemCharArrayAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + char* out, + cuopt_int_t count) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (out == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (!is_char_array_attribute(attribute)) { return CUOPT_INVALID_ARGUMENT; } + + auto* iface = get_iface(problem); + const cuopt_int_t expected = get_array_size(iface, attribute); + if (expected < 0 || count != expected) { return CUOPT_INVALID_ARGUMENT; } + + if (attribute == CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE) { + const std::vector row_types = iface->get_row_types_host(); + if (row_types.size() != expected) { return CUOPT_VALIDATION_ERROR; } + std::copy(row_types.begin(), row_types.end(), out); + } else if (attribute == CUOPT_ARRAY_ATTR_VARIABLE_TYPES) { + const std::vector var_types = iface->get_variable_types_host(); + if (var_types.size() != expected) { return CUOPT_VALIDATION_ERROR; } + for (cuopt_int_t i = 0; i < count; ++i) { + out[i] = var_type_to_char(var_types[i]); + } + } else { + return CUOPT_INVALID_ARGUMENT; + } + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptGetProblemStringArrayAttribute(cuOptOptimizationProblem problem, + cuopt_int_t attribute, + const char** strings_out, + cuopt_int_t count) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (strings_out == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (attribute != CUOPT_STRING_ARRAY_VARIABLE_NAMES && attribute != CUOPT_STRING_ARRAY_ROW_NAMES) { + return CUOPT_INVALID_ARGUMENT; + } + + auto* iface = get_iface(problem); + const auto& names = (attribute == CUOPT_STRING_ARRAY_VARIABLE_NAMES) ? iface->get_variable_names() + : iface->get_row_names(); + + if (names.size() != count) { return CUOPT_INVALID_ARGUMENT; } + for (cuopt_int_t i = 0; i < count; ++i) { + strings_out[i] = names[i].c_str(); + } + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptGetConstraintMatrixCSR(cuOptOptimizationProblem problem, + cuopt_int_t* constraint_matrix_row_offsets_ptr, + cuopt_int_t* constraint_matrix_column_indices_ptr, + cuopt_float_t* constraint_matrix_coefficients_ptr) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (constraint_matrix_row_offsets_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (constraint_matrix_column_indices_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (constraint_matrix_coefficients_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + + auto* iface = get_iface(problem); + cuopt_int_t num_nnz = iface->get_nnz(); + cuopt_int_t num_rows = iface->get_n_constraints(); + + iface->copy_constraint_matrix_to_host(constraint_matrix_coefficients_ptr, + constraint_matrix_column_indices_ptr, + constraint_matrix_row_offsets_ptr, + num_nnz, + num_nnz, + num_rows + 1); + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptGetConstraintMatrixCSC(cuOptOptimizationProblem problem, + cuopt_int_t* column_offsets_ptr, + cuopt_int_t* row_indices_ptr, + cuopt_float_t* values_ptr) +{ + if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (column_offsets_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + + auto* iface = get_iface(problem); + const cuopt_int_t n = iface->get_n_variables(); + const cuopt_int_t m = iface->get_n_constraints(); + + std::vector row_offsets = iface->get_constraint_matrix_offsets_host(); + std::vector col_indices = iface->get_constraint_matrix_indices_host(); + std::vector values = iface->get_constraint_matrix_values_host(); + const cuopt_int_t nnz = static_cast(values.size()); + + // Empty / unset matrix: emit all-zero column offsets and nothing else. + if (row_offsets.size() < m + 1 || nnz == 0) { + std::fill(column_offsets_ptr, column_offsets_ptr + (n + 1), 0); + return CUOPT_SUCCESS; + } + if (row_indices_ptr == nullptr || values_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + + csr_matrix_t csr(m, n, nnz); + csr.row_start = std::move(row_offsets); + csr.j = std::move(col_indices); + csr.x = std::move(values); + + csc_matrix_t csc(m, n, nnz); + csr.to_compressed_col(csc); + + std::copy(csc.col_start.begin(), csc.col_start.end(), column_offsets_ptr); + std::copy(csc.i.begin(), csc.i.end(), row_indices_ptr); + std::copy(csc.x.begin(), csc.x.end(), values_ptr); + return CUOPT_SUCCESS; +} 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 467af308b7..fb5ac8c573 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 @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -25,8 +26,11 @@ namespace cuopt::mathematical_optimization::pdlp { bool is_cusparse_runtime_mixed_precision_supported(); } +#include #include +using ::testing::ElementsAreArray; + TEST(c_api, int_size) { EXPECT_EQ(test_int_size(), sizeof(int32_t)); } TEST(c_api, float_size) { EXPECT_EQ(test_float_size(), sizeof(double)); } @@ -780,5 +784,287 @@ TEST(c_api, gpu_problem_rejects_remote_after_create) EXPECT_EQ(test_gpu_problem_remote_after_create(lp_file.c_str()), CUOPT_SUCCESS); } +// Attribute getters are checked against the exact values passed to the cuOptCreate* / cuOptSet* +// interfaces. cuOptReadProblem is used only for variable/row names, which no create/set routine +// sets. +TEST(c_api, problem_attributes_created) +{ + // A small mixed-integer LP, built by hand so every getter can be compared to a known value: + // A = [ 1 0 2 ] sense L, rhs 10 min 5 + [1,2,3].x var types [I,C,I] + // [ 0 3 4 ] sense G, rhs 20 + const cuopt_int_t num_constraints = 2; + const cuopt_int_t num_variables = 3; + const cuopt_int_t nnz = 4; + const cuopt_float_t objective_offset = 5.0; + const cuopt_float_t objective_coefficients[3] = {1.0, 2.0, 3.0}; + const cuopt_int_t row_offsets[3] = {0, 2, 4}; + const cuopt_int_t col_indices[4] = {0, 2, 1, 2}; + const cuopt_float_t matrix_values[4] = {1.0, 2.0, 3.0, 4.0}; + const char constraint_sense[2] = {CUOPT_LESS_THAN, CUOPT_GREATER_THAN}; + const cuopt_float_t rhs[2] = {10.0, 20.0}; + const cuopt_float_t lower_bounds[3] = {0.0, 0.0, 0.0}; + const cuopt_float_t upper_bounds[3] = {100.0, 100.0, 100.0}; + const char variable_types[3] = {CUOPT_INTEGER, CUOPT_CONTINUOUS, CUOPT_INTEGER}; + + // CSC is the transpose of the CSR above, written out by hand for a direct comparison. + const cuopt_int_t csc_offsets[4] = {0, 1, 2, 4}; + const cuopt_int_t csc_row_indices[4] = {0, 1, 0, 1}; + const cuopt_float_t csc_values[4] = {1.0, 3.0, 2.0, 4.0}; + + cuOptOptimizationProblem problem = nullptr; + ASSERT_EQ(cuOptCreateProblem(num_constraints, + num_variables, + CUOPT_MINIMIZE, + objective_offset, + objective_coefficients, + row_offsets, + col_indices, + matrix_values, + constraint_sense, + rhs, + lower_bounds, + upper_bounds, + variable_types, + &problem), + CUOPT_SUCCESS); + + auto get_int = [&](cuopt_int_t attr) { + cuopt_int_t v = -1; + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, attr, &v), CUOPT_SUCCESS); + return v; + }; + auto get_float = [&](cuopt_int_t attr) { + cuopt_float_t v = 0.0; + EXPECT_EQ(cuOptGetProblemFloatAttribute(problem, attr, &v), CUOPT_SUCCESS); + return v; + }; + + EXPECT_EQ(get_int(CUOPT_ATTR_NUM_VARIABLES), num_variables); + EXPECT_EQ(get_int(CUOPT_ATTR_NUM_CONSTRAINTS), num_constraints); + EXPECT_EQ(get_int(CUOPT_ATTR_NUM_NONZEROS), nnz); + EXPECT_EQ(get_int(CUOPT_ATTR_NUM_INTEGERS), 2); + EXPECT_EQ(get_int(CUOPT_ATTR_PROBLEM_CATEGORY), 1 /* MIP */); + EXPECT_EQ(get_int(CUOPT_ATTR_IS_MIP), 1); + EXPECT_EQ(get_int(CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE), 0); + EXPECT_EQ(get_int(CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS), 0); + EXPECT_EQ(get_int(CUOPT_ATTR_OBJECTIVE_SENSE), CUOPT_MINIMIZE); + EXPECT_EQ(get_float(CUOPT_ATTR_OBJECTIVE_OFFSET), objective_offset); + EXPECT_EQ(get_float(CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR), 1.0); // create leaves the default of 1 + + std::vector fbuf(num_variables); + EXPECT_EQ(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, fbuf.data(), num_variables), + CUOPT_SUCCESS); + EXPECT_THAT(fbuf, ElementsAreArray(objective_coefficients)); + EXPECT_EQ(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, fbuf.data(), num_variables), + CUOPT_SUCCESS); + EXPECT_THAT(fbuf, ElementsAreArray(lower_bounds)); + EXPECT_EQ(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, fbuf.data(), num_variables), + CUOPT_SUCCESS); + EXPECT_THAT(fbuf, ElementsAreArray(upper_bounds)); + + std::vector rhs_buf(num_constraints); + EXPECT_EQ(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, rhs_buf.data(), num_constraints), + CUOPT_SUCCESS); + EXPECT_THAT(rhs_buf, ElementsAreArray(rhs)); + + std::vector sense_buf(num_constraints); + EXPECT_EQ(cuOptGetProblemCharArrayAttribute( + problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, sense_buf.data(), num_constraints), + CUOPT_SUCCESS); + EXPECT_THAT(sense_buf, ElementsAreArray(constraint_sense)); + std::vector type_buf(num_variables); + EXPECT_EQ(cuOptGetProblemCharArrayAttribute( + problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, type_buf.data(), num_variables), + CUOPT_SUCCESS); + EXPECT_THAT(type_buf, ElementsAreArray(variable_types)); + + // CSR getter must return exactly the matrix we created with. + std::vector csr_off(num_constraints + 1), csr_col(nnz); + std::vector csr_val(nnz); + EXPECT_EQ(cuOptGetConstraintMatrixCSR(problem, csr_off.data(), csr_col.data(), csr_val.data()), + CUOPT_SUCCESS); + EXPECT_THAT(csr_off, ElementsAreArray(row_offsets)); + EXPECT_THAT(csr_col, ElementsAreArray(col_indices)); + EXPECT_THAT(csr_val, ElementsAreArray(matrix_values)); + + // CSC getter returns the transpose; compare against the hand-written expected layout. + std::vector csc_off(num_variables + 1), csc_row(nnz); + std::vector csc_val(nnz); + EXPECT_EQ(cuOptGetConstraintMatrixCSC(problem, csc_off.data(), csc_row.data(), csc_val.data()), + CUOPT_SUCCESS); + EXPECT_THAT(csc_off, ElementsAreArray(csc_offsets)); + EXPECT_THAT(csc_row, ElementsAreArray(csc_row_indices)); + EXPECT_THAT(csc_val, ElementsAreArray(csc_values)); + + // Names are not set by create; requesting them must be rejected. + const char* names[3] = {nullptr, nullptr, nullptr}; + EXPECT_EQ(cuOptGetProblemStringArrayAttribute( + problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, names, num_variables), + CUOPT_INVALID_ARGUMENT); + + cuOptDestroyProblem(&problem); +} + +// Ranged rows can only be built with cuOptCreateRangedProblem, so it is the only path that sets the +// constraint lower/upper bound attributes. +TEST(c_api, problem_attributes_ranged) +{ + const cuopt_int_t num_constraints = 2; + const cuopt_int_t num_variables = 2; + const cuopt_float_t objective_coefficients[2] = {1.0, 1.0}; + const cuopt_int_t row_offsets[3] = {0, 2, 3}; + const cuopt_int_t col_indices[3] = {0, 1, 0}; + const cuopt_float_t matrix_values[3] = {1.0, 1.0, 1.0}; + const cuopt_float_t constraint_lower_bounds[2] = {1.0, 0.0}; + const cuopt_float_t constraint_upper_bounds[2] = {10.0, 5.0}; + const cuopt_float_t variable_lower_bounds[2] = {0.0, 0.0}; + const cuopt_float_t variable_upper_bounds[2] = {100.0, 100.0}; + const char variable_types[2] = {CUOPT_CONTINUOUS, CUOPT_CONTINUOUS}; + + cuOptOptimizationProblem problem = nullptr; + ASSERT_EQ(cuOptCreateRangedProblem(num_constraints, + num_variables, + CUOPT_MINIMIZE, + 0.0, + objective_coefficients, + row_offsets, + col_indices, + matrix_values, + constraint_lower_bounds, + constraint_upper_bounds, + variable_lower_bounds, + variable_upper_bounds, + variable_types, + &problem), + CUOPT_SUCCESS); + + std::vector buf(num_constraints); + EXPECT_EQ(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS, buf.data(), num_constraints), + CUOPT_SUCCESS); + EXPECT_THAT(buf, ElementsAreArray(constraint_lower_bounds)); + EXPECT_EQ(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS, buf.data(), num_constraints), + CUOPT_SUCCESS); + EXPECT_THAT(buf, ElementsAreArray(constraint_upper_bounds)); + + cuOptDestroyProblem(&problem); +} + +// The quadratic-presence flags flip only after cuOptSetQuadraticObjective / +// cuOptAddQuadraticConstraint. +TEST(c_api, problem_attributes_quadratic) +{ + const cuopt_int_t num_constraints = 1; + const cuopt_int_t num_variables = 2; + const cuopt_float_t objective_coefficients[2] = {1.0, 1.0}; + const cuopt_int_t row_offsets[2] = {0, 2}; + const cuopt_int_t col_indices[2] = {0, 1}; + const cuopt_float_t matrix_values[2] = {1.0, 1.0}; + const char constraint_sense[1] = {CUOPT_LESS_THAN}; + const cuopt_float_t rhs[1] = {10.0}; + const cuopt_float_t lower_bounds[2] = {0.0, 0.0}; + const cuopt_float_t upper_bounds[2] = {100.0, 100.0}; + const char variable_types[2] = {CUOPT_CONTINUOUS, CUOPT_CONTINUOUS}; + + cuOptOptimizationProblem problem = nullptr; + ASSERT_EQ(cuOptCreateProblem(num_constraints, + num_variables, + CUOPT_MINIMIZE, + 0.0, + objective_coefficients, + row_offsets, + col_indices, + matrix_values, + constraint_sense, + rhs, + lower_bounds, + upper_bounds, + variable_types, + &problem), + CUOPT_SUCCESS); + + // Fresh sentinel-initialized read on every call, so a getter that fails to write is always + // caught. + auto get_int = [&](cuopt_int_t attr) { + cuopt_int_t v = -1; + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, attr, &v), CUOPT_SUCCESS); + return v; + }; + + // Purely linear to start: both presence flags read 0. + EXPECT_EQ(get_int(CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE), 0); + EXPECT_EQ(get_int(CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS), 0); + + // Add a quadratic objective term 2 * x0^2. + const cuopt_int_t q_row[1] = {0}; + const cuopt_int_t q_col[1] = {0}; + const cuopt_float_t q_val[1] = {2.0}; + ASSERT_EQ(cuOptSetQuadraticObjective(problem, 1, q_row, q_col, q_val), CUOPT_SUCCESS); + EXPECT_EQ(get_int(CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE), 1); + + // Add a quadratic constraint x1^2 + x0 <= 5. + const cuopt_int_t qc_row[1] = {1}; + const cuopt_int_t qc_col[1] = {1}; + const cuopt_float_t qc_val[1] = {1.0}; + const cuopt_int_t qc_lin_idx[1] = {0}; + const cuopt_float_t qc_lin_coeff[1] = {1.0}; + ASSERT_EQ( + cuOptAddQuadraticConstraint( + problem, 1, qc_row, qc_col, qc_val, 1, qc_lin_idx, qc_lin_coeff, CUOPT_LESS_THAN, 5.0), + CUOPT_SUCCESS); + EXPECT_EQ(get_int(CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS), 1); + + cuOptDestroyProblem(&problem); +} + +// Variable/row names are the only attributes no create/set routine can set, so read a tiny MPS with +// known names and check them back. +TEST(c_api, problem_attributes_names) +{ + const std::string mps_path = + std::filesystem::temp_directory_path().string() + "/cuopt_attr_names.mps"; + { + std::ofstream out(mps_path); + out << "NAME TOY\n" + "ROWS\n" + " N COST\n" + " L C1\n" + " G C2\n" + "COLUMNS\n" + " X1 COST 1.0 C1 1.0\n" + " X1 C2 1.0\n" + " X2 COST 2.0 C1 3.0\n" + " X2 C2 1.0\n" + "RHS\n" + " RHS C1 10.0 C2 2.0\n" + "ENDATA\n"; + } + + cuOptOptimizationProblem problem = nullptr; + ASSERT_EQ(cuOptReadProblem(mps_path.c_str(), &problem), CUOPT_SUCCESS); + + const char* var_names[2] = {nullptr, nullptr}; + ASSERT_EQ( + cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, var_names, 2), + CUOPT_SUCCESS); + EXPECT_STREQ(var_names[0], "X1"); + EXPECT_STREQ(var_names[1], "X2"); + + const char* row_names[2] = {nullptr, nullptr}; + ASSERT_EQ( + cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_ROW_NAMES, row_names, 2), + CUOPT_SUCCESS); + EXPECT_STREQ(row_names[0], "C1"); + EXPECT_STREQ(row_names[1], "C2"); + + cuOptDestroyProblem(&problem); + std::filesystem::remove(mps_path); +} + // Note: cuopt_cli subprocess tests are in Python (test_cpu_only_execution.py) // which provides better cross-platform subprocess handling