From 7d220df3e9c455fd02ba5474bb7361e3f1d31ae8 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 7 Jul 2026 10:27:27 -0400 Subject: [PATCH 1/6] extend getters on C API --- .../mathematical_optimization/constants.h | 37 ++ .../cuopt/mathematical_optimization/cuopt_c.h | 88 ++++- cpp/src/pdlp/CMakeLists.txt | 1 + cpp/src/pdlp/cuopt_c_attributes.cpp | 264 ++++++++++++++ .../c_api_tests/c_api_test.c | 327 ++++++++++++++++++ .../c_api_tests/c_api_tests.cpp | 21 ++ .../c_api_tests/c_api_tests.h | 5 + 7 files changed, 742 insertions(+), 1 deletion(-) create mode 100644 cpp/src/pdlp/cuopt_c_attributes.cpp diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index f6be07aaa9..64f34bea90 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -225,4 +225,41 @@ #define CUOPT_BARRIER_ITERATIVE_REFINEMENT_OFF 0 #define CUOPT_BARRIER_ITERATIVE_REFINEMENT_ON 1 +/* @brief Scalar problem attribute selectors (see cuOptGetProblem{Int,Float,String}Attribute). + * 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 +/* NOTE: HAS_QUADRATIC_OBJECTIVE / HAS_QUADRATIC_CONSTRAINTS report presence only. The quadratic + * objective matrix (Q) and quadratic constraint data are not currently retrievable through any + * getter */ + +/* @brief Numeric/char array problem attribute selectors + * (see cuOptGetProblem{Float,Char}ArrayAttribute; sized by num_variables / num_constraints). + * The constraint matrix is retrieved via cuOptGetConstraintMatrix / cuOptGetConstraintMatrixCSC, + * not through these selectors. Passed as cuopt_int_t. Numbered in a separate range from the scalar + * selectors so a selector from the wrong family fails validation (there is no compile-time type). + */ +#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..0aab0adf76 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -563,7 +563,9 @@ 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; quadratic constraint terms (if any) + * are not included. * * @param[in] problem - The optimization problem. * @@ -586,6 +588,31 @@ 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 column + * format. This is the matrix of the linear constraints only; quadratic constraint terms (if any) + * are not included. + * + * @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 cuOptGetNumVariables) 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 +1082,65 @@ 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 + * cuOptGetNumVariables / cuOptGetNumConstraints). 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: quadratic data is not yet retrievable. CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE and + * CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS report presence only; 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 + * cuOptGetNumVariables / cuOptGetNumConstraints). */ +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 + * cuOptGetNumConstraints / cuOptGetNumVariables). */ +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/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index f5f26837b6..38e140341d 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -35,6 +35,7 @@ set(LP_CORE_FILES set(LP_ADAPTER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/cython_solve.cu ${CMAKE_CURRENT_SOURCE_DIR}/cuopt_c.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cuopt_c_attributes.cpp ) # Choose which files to include based on build mode diff --git a/cpp/src/pdlp/cuopt_c_attributes.cpp b/cpp/src/pdlp/cuopt_c_attributes.cpp new file mode 100644 index 0000000000..47be03d8b5 --- /dev/null +++ b/cpp/src/pdlp/cuopt_c_attributes.cpp @@ -0,0 +1,264 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#include +#include +#include + +#include +#include +#include + +using namespace cuopt::mathematical_optimization; + +namespace { + +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; +} + +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; + } +} + +} // namespace + +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; } + + 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 (static_cast(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; } + + 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 (static_cast(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 (static_cast(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[static_cast(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 (count != static_cast(names.size())) { return CUOPT_INVALID_ARGUMENT; } + for (cuopt_int_t i = 0; i < count; ++i) { + strings_out[i] = names[static_cast(i)].c_str(); + } + 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(); + + const std::vector row_offsets = iface->get_constraint_matrix_offsets_host(); + const std::vector col_indices = iface->get_constraint_matrix_indices_host(); + const 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() < static_cast(m + 1) || nnz == 0) { + for (cuopt_int_t c = 0; c <= n; ++c) { + column_offsets_ptr[c] = 0; + } + return CUOPT_SUCCESS; + } + if (row_indices_ptr == nullptr || values_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + + // Count non-zeros per column, then prefix-sum into column offsets. + std::vector col_counts(static_cast(n), 0); + for (cuopt_int_t k = 0; k < nnz; ++k) { + const cuopt_int_t c = col_indices[static_cast(k)]; + if (c < 0 || c >= n) { return CUOPT_VALIDATION_ERROR; } + ++col_counts[static_cast(c)]; + } + column_offsets_ptr[0] = 0; + for (cuopt_int_t c = 0; c < n; ++c) { + column_offsets_ptr[c + 1] = column_offsets_ptr[c] + col_counts[static_cast(c)]; + } + + // Scatter each CSR entry into its CSC position using a running write cursor per column. + std::vector next(column_offsets_ptr, column_offsets_ptr + n); + for (cuopt_int_t i = 0; i < m; ++i) { + const cuopt_int_t row_begin = row_offsets[static_cast(i)]; + const cuopt_int_t row_end = row_offsets[static_cast(i + 1)]; + for (cuopt_int_t k = row_begin; k < row_end; ++k) { + const cuopt_int_t c = col_indices[static_cast(k)]; + const cuopt_int_t dest = next[static_cast(c)]++; + row_indices_ptr[dest] = i; + values_ptr[dest] = values[static_cast(k)]; + } + } + return CUOPT_SUCCESS; +} 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 cc8d9c842c..ab8fc8fad1 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 @@ -3166,3 +3166,330 @@ cuopt_int_t test_gpu_problem_remote_after_create(const char* filename) return status; } + +/* Key scalar facts filled by read_all_problem_attributes(), used by category-specific tests. */ +typedef struct { + cuopt_int_t n_variables; + cuopt_int_t n_constraints; + cuopt_int_t n_nonzeros; + cuopt_int_t n_integers; + cuopt_int_t category; + cuopt_int_t is_mip; + cuopt_int_t has_quadratic_objective; + cuopt_int_t has_quadratic_constraints; + cuopt_int_t num_integer_type_chars; /* count of 'I' in variable_types */ +} problem_facts_t; + +static int doubles_equal(const cuopt_float_t* a, const cuopt_float_t* b, cuopt_int_t n) +{ + cuopt_int_t i; + for (i = 0; i < n; ++i) { + if (a[i] != b[i]) { return 0; } + } + return 1; +} + +/* + * Read back every attribute getter and cross-check it against the dedicated accessor / the other + * matrix layout. Fills *facts on success. Returns CUOPT_SUCCESS or the first failing status. + */ +static cuopt_int_t read_all_problem_attributes(const char* filename, problem_facts_t* facts) +{ +#define ATTR_CHECK(call) \ + do { \ + cuopt_int_t _s = (call); \ + if (_s != CUOPT_SUCCESS) { \ + printf("FAILED (%d): %s\n", (int)_s, #call); \ + status = _s; \ + goto DONE; \ + } \ + } while (0) + + cuOptOptimizationProblem problem = NULL; + cuopt_int_t status = CUOPT_SUCCESS; + + cuopt_float_t* obj_g = NULL; /* _g = via generic attribute getter */ + cuopt_float_t* obj_d = NULL; /* _d = via dedicated getter */ + cuopt_float_t* vlb_g = NULL; + cuopt_float_t* vlb_d = NULL; + cuopt_float_t* vub_g = NULL; + cuopt_float_t* vub_d = NULL; + cuopt_float_t* rhs_g = NULL; + cuopt_float_t* rhs_d = NULL; + cuopt_float_t* clb_g = NULL; + cuopt_float_t* cub_g = NULL; + char* sense_g = NULL; + char* sense_d = NULL; + char* types_g = NULL; + char* types_d = NULL; + const char** var_names = NULL; + const char** row_names = NULL; + cuopt_int_t* csr_off = NULL; + cuopt_int_t* csr_col = NULL; + cuopt_float_t* csr_val = NULL; + cuopt_int_t* csc_off = NULL; + cuopt_int_t* csc_row = NULL; + cuopt_float_t* csc_val = NULL; + cuopt_int_t* col_counts = NULL; + + cuopt_int_t nv = 0, nc = 0, nnz = 0; + cuopt_int_t a_nv = 0, a_nc = 0, a_nnz = 0, a_sense = 0, d_sense = 0; + cuopt_float_t f_offset = 0.0, f_scale = 0.0, d_offset = 0.0; + cuopt_int_t i = 0, k = 0; + + ATTR_CHECK(cuOptReadProblem(filename, &problem)); + + /* --- dimensions (used to size all arrays) --- */ + ATTR_CHECK(cuOptGetNumVariables(problem, &nv)); + ATTR_CHECK(cuOptGetNumConstraints(problem, &nc)); + ATTR_CHECK(cuOptGetNumNonZeros(problem, &nnz)); + + /* --- scalar int attributes, cross-checked against dedicated getters where they exist --- */ + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &a_nv)); + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &a_nc)); + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &a_nnz)); + if (a_nv != nv || a_nc != nc || a_nnz != nnz) { + printf("Scalar dimension attribute mismatch\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &facts->n_integers)); + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_PROBLEM_CATEGORY, &facts->category)); + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &facts->is_mip)); + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, + &facts->has_quadratic_objective)); + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, + &facts->has_quadratic_constraints)); + ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_OBJECTIVE_SENSE, &a_sense)); + ATTR_CHECK(cuOptGetObjectiveSense(problem, &d_sense)); + if (a_sense != d_sense) { + printf("Objective sense mismatch (generic %d vs dedicated %d)\n", (int)a_sense, (int)d_sense); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + + /* --- scalar float attributes --- */ + ATTR_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &f_offset)); + ATTR_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &f_scale)); + ATTR_CHECK(cuOptGetObjectiveOffset(problem, &d_offset)); + if (f_offset != d_offset) { + printf("Objective offset mismatch (generic %g vs dedicated %g)\n", f_offset, d_offset); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + (void)f_scale; + + /* --- float arrays: generic getter must match dedicated getter byte-for-byte --- */ + obj_g = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); + obj_d = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); + vlb_g = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); + vlb_d = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); + vub_g = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); + vub_d = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); + rhs_g = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); + rhs_d = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); + if (!obj_g || !obj_d || !vlb_g || !vlb_d || !vub_g || !vub_d || !rhs_g || !rhs_d) { + status = CUOPT_OUT_OF_MEMORY; + goto DONE; + } + ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, + obj_g, nv)); + ATTR_CHECK(cuOptGetObjectiveCoefficients(problem, obj_d)); + ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, + vlb_g, nv)); + ATTR_CHECK(cuOptGetVariableLowerBounds(problem, vlb_d)); + ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, + vub_g, nv)); + ATTR_CHECK(cuOptGetVariableUpperBounds(problem, vub_d)); + ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, rhs_g, nc)); + ATTR_CHECK(cuOptGetConstraintRightHandSide(problem, rhs_d)); + if (!doubles_equal(obj_g, obj_d, nv) || !doubles_equal(vlb_g, vlb_d, nv) || + !doubles_equal(vub_g, vub_d, nv) || !doubles_equal(rhs_g, rhs_d, nc)) { + printf("Generic float array getter disagrees with dedicated getter\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + + /* --- constraint lower/upper bounds: optional (ranged models only). Exercise the code path; + accept SUCCESS (populated) or CUOPT_VALIDATION_ERROR (empty for a sense+rhs model). --- */ + clb_g = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); + cub_g = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); + if (!clb_g || !cub_g) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } + { + cuopt_int_t s_lb = + cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS, clb_g, nc); + cuopt_int_t s_ub = + cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS, cub_g, nc); + if ((s_lb != CUOPT_SUCCESS && s_lb != CUOPT_VALIDATION_ERROR) || + (s_ub != CUOPT_SUCCESS && s_ub != CUOPT_VALIDATION_ERROR)) { + printf("Constraint bound getters returned unexpected status (%d, %d)\n", (int)s_lb, (int)s_ub); + status = (s_lb != CUOPT_SUCCESS && s_lb != CUOPT_VALIDATION_ERROR) ? s_lb : s_ub; + goto DONE; + } + } + + /* --- char arrays: generic must match dedicated --- */ + sense_g = (char*)malloc((size_t)nc * sizeof(char)); + sense_d = (char*)malloc((size_t)nc * sizeof(char)); + types_g = (char*)malloc((size_t)nv * sizeof(char)); + types_d = (char*)malloc((size_t)nv * sizeof(char)); + if (!sense_g || !sense_d || !types_g || !types_d) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } + ATTR_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, sense_g, + nc)); + ATTR_CHECK(cuOptGetConstraintSense(problem, sense_d)); + ATTR_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, types_g, + nv)); + ATTR_CHECK(cuOptGetVariableTypes(problem, types_d)); + for (i = 0; i < nc; ++i) { + if (sense_g[i] != sense_d[i]) { + printf("Constraint sense mismatch at %d\n", (int)i); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + } + facts->num_integer_type_chars = 0; + for (i = 0; i < nv; ++i) { + if (types_g[i] != types_d[i]) { + printf("Variable type mismatch at %d\n", (int)i); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + if (types_g[i] == CUOPT_INTEGER) { facts->num_integer_type_chars++; } + } + + /* --- string arrays: borrowed pointers into cuOpt storage --- */ + var_names = (const char**)malloc((size_t)nv * sizeof(const char*)); + row_names = (const char**)malloc((size_t)nc * sizeof(const char*)); + if (!var_names || !row_names) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } + ATTR_CHECK(cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, + var_names, nv)); + ATTR_CHECK(cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_ROW_NAMES, row_names, + nc)); + if (nv > 0 && var_names[0] == NULL) { + printf("Expected non-null variable name pointers\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + if (nc > 0 && row_names[0] == NULL) { + printf("Expected non-null row name pointers\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + + /* --- constraint matrix: CSR (dedicated) and CSC (new), cross-checked --- */ + csr_off = (cuopt_int_t*)malloc((size_t)(nc + 1) * sizeof(cuopt_int_t)); + csr_col = (cuopt_int_t*)malloc((size_t)nnz * sizeof(cuopt_int_t)); + csr_val = (cuopt_float_t*)malloc((size_t)nnz * sizeof(cuopt_float_t)); + csc_off = (cuopt_int_t*)malloc((size_t)(nv + 1) * sizeof(cuopt_int_t)); + csc_row = (cuopt_int_t*)malloc((size_t)nnz * sizeof(cuopt_int_t)); + csc_val = (cuopt_float_t*)malloc((size_t)nnz * sizeof(cuopt_float_t)); + col_counts = (cuopt_int_t*)calloc((size_t)(nv > 0 ? nv : 1), sizeof(cuopt_int_t)); + if (!csr_off || !csr_col || !csr_val || !csc_off || !csc_row || !csc_val || !col_counts) { + status = CUOPT_OUT_OF_MEMORY; + goto DONE; + } + ATTR_CHECK(cuOptGetConstraintMatrix(problem, csr_off, csr_col, csr_val)); + ATTR_CHECK(cuOptGetConstraintMatrixCSC(problem, csc_off, csc_row, csc_val)); + if (csr_off[nc] != nnz || csc_off[nv] != nnz || csr_off[0] != 0 || csc_off[0] != 0) { + printf("CSR/CSC offsets inconsistent with nnz\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + /* Independent transpose check: per-column nnz from CSR column indices must equal the gaps in + the CSC column offsets. */ + for (k = 0; k < nnz; ++k) { + if (csr_col[k] < 0 || csr_col[k] >= nv) { + printf("CSR column index out of range\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + col_counts[csr_col[k]]++; + } + for (i = 0; i < nv; ++i) { + if (csc_off[i + 1] - csc_off[i] != col_counts[i]) { + printf("CSC column %d count disagrees with CSR transpose\n", (int)i); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + } + + facts->n_variables = nv; + facts->n_constraints = nc; + facts->n_nonzeros = nnz; + +DONE: + cuOptDestroyProblem(&problem); + free(obj_g); + free(obj_d); + free(vlb_g); + free(vlb_d); + free(vub_g); + free(vub_d); + free(rhs_g); + free(rhs_d); + free(clb_g); + free(cub_g); + free(sense_g); + free(sense_d); + free(types_g); + free(types_d); + free(var_names); + free(row_names); + free(csr_off); + free(csr_col); + free(csr_val); + free(csc_off); + free(csc_row); + free(csc_val); + free(col_counts); + return status; +#undef ATTR_CHECK +} + +/* LP entry point: read back every getter and verify internal consistency. */ +cuopt_int_t test_problem_attributes(const char* filename) +{ + problem_facts_t facts = {0}; + return read_all_problem_attributes(filename, &facts); +} + +/* MIP/IP entry point: full read plus MIP-specific expectations. */ +cuopt_int_t test_problem_attributes_mip(const char* filename) +{ + problem_facts_t facts = {0}; + cuopt_int_t status = read_all_problem_attributes(filename, &facts); + if (status != CUOPT_SUCCESS) { return status; } + + if (facts.is_mip != 1) { + printf("Expected is_mip=1 for MIP file\n"); + return CUOPT_VALIDATION_ERROR; + } + /* category is MIP (1) or IP (2). */ + if (facts.category != 1 && facts.category != 2) { + printf("Expected MIP/IP category, got %d\n", (int)facts.category); + return CUOPT_VALIDATION_ERROR; + } + if (facts.n_integers <= 0) { + printf("Expected num_integers > 0 for MIP file\n"); + return CUOPT_VALIDATION_ERROR; + } + if (facts.num_integer_type_chars <= 0) { + printf("Expected at least one integer variable type\n"); + return CUOPT_VALIDATION_ERROR; + } + return CUOPT_SUCCESS; +} + +/* QP entry point: full read plus quadratic-objective expectation. */ +cuopt_int_t test_problem_attributes_qp(const char* filename) +{ + problem_facts_t facts = {0}; + cuopt_int_t status = read_all_problem_attributes(filename, &facts); + if (status != CUOPT_SUCCESS) { return status; } + + if (facts.has_quadratic_objective != 1) { + printf("Expected has_quadratic_objective=1 for QP file\n"); + return CUOPT_VALIDATION_ERROR; + } + 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..85fa345761 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 @@ -780,5 +780,26 @@ TEST(c_api, gpu_problem_rejects_remote_after_create) EXPECT_EQ(test_gpu_problem_remote_after_create(lp_file.c_str()), CUOPT_SUCCESS); } +TEST(c_api, problem_attributes) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + std::string filename = rapidsDatasetRootDir + "/linear_programming/afiro_original.mps"; + EXPECT_EQ(test_problem_attributes(filename.c_str()), CUOPT_SUCCESS); +} + +TEST(c_api, problem_attributes_mip) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + std::string filename = rapidsDatasetRootDir + "/mip/50v-10.mps"; + EXPECT_EQ(test_problem_attributes_mip(filename.c_str()), CUOPT_SUCCESS); +} + +TEST(c_api, problem_attributes_qp) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + std::string filename = rapidsDatasetRootDir + "/quadratic_programming/QP_Test_1.qps"; + EXPECT_EQ(test_problem_attributes_qp(filename.c_str()), CUOPT_SUCCESS); +} + // Note: cuopt_cli subprocess tests are in Python (test_cpu_only_execution.py) // which provides better cross-platform subprocess handling 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 a8c3a1f4e4..580c92a8a8 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 @@ -87,6 +87,11 @@ cuopt_int_t test_cpu_host_create_problem_api(); /* GPU-backed problem created before remote env is set must reject remote solve */ cuopt_int_t test_gpu_problem_remote_after_create(const char* filename); +/* Generic problem attribute getters */ +cuopt_int_t test_problem_attributes(const char* filename); +cuopt_int_t test_problem_attributes_mip(const char* filename); +cuopt_int_t test_problem_attributes_qp(const char* filename); + #ifdef __cplusplus } #endif From ab4226d415b1a130ac542846516db44880ce39dd Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 10 Jul 2026 14:40:59 -0400 Subject: [PATCH 2/6] C API extensions improve tests and style consistency --- .../mathematical_optimization/constants.h | 2 +- cpp/src/pdlp/cuopt_c_attributes.cpp | 21 ++ .../c_api_tests/c_api_test.c | 261 ++++++++++++++++++ .../c_api_tests/c_api_tests.cpp | 188 +++++++++++++ .../c_api_tests/c_api_tests.h | 3 + 5 files changed, 474 insertions(+), 1 deletion(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 64f34bea90..db93b88bd4 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -225,7 +225,7 @@ #define CUOPT_BARRIER_ITERATIVE_REFINEMENT_OFF 0 #define CUOPT_BARRIER_ITERATIVE_REFINEMENT_ON 1 -/* @brief Scalar problem attribute selectors (see cuOptGetProblem{Int,Float,String}Attribute). +/* @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 diff --git a/cpp/src/pdlp/cuopt_c_attributes.cpp b/cpp/src/pdlp/cuopt_c_attributes.cpp index 47be03d8b5..587a68c71e 100644 --- a/cpp/src/pdlp/cuopt_c_attributes.cpp +++ b/cpp/src/pdlp/cuopt_c_attributes.cpp @@ -51,6 +51,25 @@ bool is_float_attribute(cuopt_int_t attribute) 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) { @@ -129,6 +148,7 @@ cuopt_int_t cuOptGetProblemFloatArrayAttribute(cuOptOptimizationProblem problem, { 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); @@ -167,6 +187,7 @@ cuopt_int_t cuOptGetProblemCharArrayAttribute(cuOptOptimizationProblem problem, { 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); 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 ab8fc8fad1..9a718280c2 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 @@ -3189,6 +3189,24 @@ static int doubles_equal(const cuopt_float_t* a, const cuopt_float_t* b, cuopt_i return 1; } +/* One sparse matrix entry, used to compare CSR and CSC as an unordered (row,col,value) multiset. */ +typedef struct { + cuopt_int_t row; + cuopt_int_t col; + cuopt_float_t val; +} coo_triple_t; + +/* Total order by (row, col, value) so two equivalent matrices sort to identical sequences. */ +static int compare_coo(const void* a, const void* b) +{ + const coo_triple_t* ta = (const coo_triple_t*)a; + const coo_triple_t* tb = (const coo_triple_t*)b; + if (ta->row != tb->row) { return ta->row < tb->row ? -1 : 1; } + if (ta->col != tb->col) { return ta->col < tb->col ? -1 : 1; } + if (ta->val != tb->val) { return ta->val < tb->val ? -1 : 1; } + return 0; +} + /* * Read back every attribute getter and cross-check it against the dedicated accessor / the other * matrix layout. Fills *facts on success. Returns CUOPT_SUCCESS or the first failing status. @@ -3231,6 +3249,8 @@ static cuopt_int_t read_all_problem_attributes(const char* filename, problem_fac cuopt_int_t* csc_row = NULL; cuopt_float_t* csc_val = NULL; cuopt_int_t* col_counts = NULL; + coo_triple_t* csr_triples = NULL; + coo_triple_t* csc_triples = NULL; cuopt_int_t nv = 0, nc = 0, nnz = 0; cuopt_int_t a_nv = 0, a_nc = 0, a_nnz = 0, a_sense = 0, d_sense = 0; @@ -3413,6 +3433,37 @@ static cuopt_int_t read_all_problem_attributes(const char* filename, problem_fac } } + /* Full equivalence: CSR and CSC must encode the same (row, col, value) multiset. Expand each + layout to COO triples, sort both, and compare entry-by-entry (values included). This catches + mispaired values / wrong row indices that the per-column count check alone would miss. */ + csr_triples = (coo_triple_t*)malloc((size_t)nnz * sizeof(coo_triple_t)); + csc_triples = (coo_triple_t*)malloc((size_t)nnz * sizeof(coo_triple_t)); + if (!csr_triples || !csc_triples) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } + for (i = 0; i < nc; ++i) { + for (k = csr_off[i]; k < csr_off[i + 1]; ++k) { + csr_triples[k].row = i; + csr_triples[k].col = csr_col[k]; + csr_triples[k].val = csr_val[k]; + } + } + for (i = 0; i < nv; ++i) { + for (k = csc_off[i]; k < csc_off[i + 1]; ++k) { + csc_triples[k].row = csc_row[k]; + csc_triples[k].col = i; + csc_triples[k].val = csc_val[k]; + } + } + qsort(csr_triples, (size_t)nnz, sizeof(coo_triple_t), compare_coo); + qsort(csc_triples, (size_t)nnz, sizeof(coo_triple_t), compare_coo); + for (k = 0; k < nnz; ++k) { + if (csr_triples[k].row != csc_triples[k].row || csr_triples[k].col != csc_triples[k].col || + csr_triples[k].val != csc_triples[k].val) { + printf("CSC entry %d disagrees with CSR (row/col/value)\n", (int)k); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + } + facts->n_variables = nv; facts->n_constraints = nc; facts->n_nonzeros = nnz; @@ -3442,6 +3493,8 @@ static cuopt_int_t read_all_problem_attributes(const char* filename, problem_fac free(csc_row); free(csc_val); free(col_counts); + free(csr_triples); + free(csc_triples); return status; #undef ATTR_CHECK } @@ -3493,3 +3546,211 @@ cuopt_int_t test_problem_attributes_qp(const char* filename) } return CUOPT_SUCCESS; } + +/* + * Build a small MIP by hand with cuOptCreateProblem, then read it back through the new getters and + * compare against the exact values we constructed. This is the only path that value-checks the + * attributes with no dedicated getter to cross-check (num_integers, problem_category, is_mip, + * has_quadratic_*, objective_scaling_factor) and the CSC matrix values. Needs no dataset file and + * works on either backend. + */ +cuopt_int_t test_problem_attributes_created(void) +{ +#define C_CHECK(call) \ + do { \ + cuopt_int_t _s = (call); \ + if (_s != CUOPT_SUCCESS) { \ + printf("FAILED (%d): %s\n", (int)_s, #call); \ + status = _s; \ + goto DONE; \ + } \ + } while (0) + + cuOptOptimizationProblem problem = NULL; + cuopt_int_t status = CUOPT_SUCCESS; + + /* Known construction: 2 constraints x 3 variables, mixed integer. + * A = [ 1 0 2 ] sense=L rhs=10 + * [ 0 3 4 ] sense=G rhs=20 + * obj = 5 + [1,2,3].x var types = [I,C,I] */ + 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}; + + cuopt_float_t fbuf[3]; + char cbuf[3]; + cuopt_int_t ival = 0; + cuopt_float_t fval = 0.0; + cuopt_int_t csc_off[4]; + cuopt_int_t csc_row[4]; + cuopt_float_t csc_val[4]; + coo_triple_t exp_tr[4]; + coo_triple_t got_tr[4]; + cuopt_int_t i = 0, k = 0; + const char* names_probe[3]; + + C_CHECK(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)); + + /* --- scalar attributes with NO dedicated getter: verify against the known construction --- */ + C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &ival)); + if (ival != 2) { + printf("num_integers: expected 2, got %d\n", (int)ival); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_PROBLEM_CATEGORY, &ival)); + if (ival != 1 /* MIP */) { + printf("problem_category: expected MIP(1), got %d\n", (int)ival); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &ival)); + if (ival != 1) { + printf("is_mip: expected 1, got %d\n", (int)ival); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival)); + if (ival != 0) { + printf("has_quadratic_objective: expected 0, got %d\n", (int)ival); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival)); + if (ival != 0) { + printf("has_quadratic_constraints: expected 0, got %d\n", (int)ival); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &fval)); + if (fval != 1.0) { /* create does not set it; default is 1 */ + printf("objective_scaling_factor: expected 1, got %g\n", fval); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &fval)); + if (fval != objective_offset) { + printf("objective_offset: expected %g, got %g\n", objective_offset, fval); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + + /* --- array getters: compare against the exact values we constructed with --- */ + C_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, fbuf, + num_variables)); + if (!doubles_equal(fbuf, objective_coefficients, num_variables)) { + printf("objective_coefficients mismatch after create\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, fbuf, + num_variables)); + if (!doubles_equal(fbuf, lower_bounds, num_variables)) { + printf("variable lower bounds mismatch after create\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, fbuf, + num_variables)); + if (!doubles_equal(fbuf, upper_bounds, num_variables)) { + printf("variable upper bounds mismatch after create\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK( + cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, fbuf, num_constraints)); + if (!doubles_equal(fbuf, rhs, num_constraints)) { + printf("rhs mismatch after create\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + C_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, cbuf, + num_constraints)); + for (i = 0; i < num_constraints; ++i) { + if (cbuf[i] != constraint_sense[i]) { + printf("constraint sense mismatch at %d after create\n", (int)i); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + } + C_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, cbuf, + num_variables)); + for (i = 0; i < num_variables; ++i) { + if (cbuf[i] != variable_types[i]) { + printf("variable type mismatch at %d after create\n", (int)i); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + } + + /* --- CSC values: must equal the transpose of the known CSR input (row,col,value multiset) --- */ + C_CHECK(cuOptGetConstraintMatrixCSC(problem, csc_off, csc_row, csc_val)); + if (csc_off[0] != 0 || csc_off[num_variables] != nnz) { + printf("CSC offsets inconsistent with nnz after create\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + for (i = 0; i < num_constraints; ++i) { + for (k = row_offsets[i]; k < row_offsets[i + 1]; ++k) { + exp_tr[k].row = i; + exp_tr[k].col = col_indices[k]; + exp_tr[k].val = matrix_values[k]; + } + } + for (i = 0; i < num_variables; ++i) { + for (k = csc_off[i]; k < csc_off[i + 1]; ++k) { + got_tr[k].row = csc_row[k]; + got_tr[k].col = i; + got_tr[k].val = csc_val[k]; + } + } + qsort(exp_tr, (size_t)nnz, sizeof(coo_triple_t), compare_coo); + qsort(got_tr, (size_t)nnz, sizeof(coo_triple_t), compare_coo); + for (k = 0; k < nnz; ++k) { + if (exp_tr[k].row != got_tr[k].row || exp_tr[k].col != got_tr[k].col || + exp_tr[k].val != got_tr[k].val) { + printf("CSC entry %d does not match constructed CSR transpose\n", (int)k); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + } + + /* cuOptCreateProblem does not set variable/row names, so the string-array getter has nothing to + return; asking for num_variables names must be rejected (count != stored 0). Name *values* can + only be verified from a parsed file (covered by the file-based tests). */ + if (cuOptGetProblemStringArrayAttribute( + problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, names_probe, num_variables) != + CUOPT_INVALID_ARGUMENT) { + printf("Expected INVALID_ARGUMENT reading names from an unnamed created problem\n"); + status = CUOPT_VALIDATION_ERROR; + goto DONE; + } + +DONE: + cuOptDestroyProblem(&problem); + return status; +#undef C_CHECK +} 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 85fa345761..faf825a832 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 @@ -14,10 +14,17 @@ #include #include +#include +#include +#include #include #include +#include +#include +#include + #include #include @@ -780,6 +787,164 @@ TEST(c_api, gpu_problem_rejects_remote_after_create) EXPECT_EQ(test_gpu_problem_remote_after_create(lp_file.c_str()), CUOPT_SUCCESS); } +namespace { + +/* + * Cross-validate the C API attribute getters against an independent source of truth: parse the same + * file directly with the C++ MPS/QPS parser (mps_data_model_t) and compare. This value-checks the + * getters that cuOptCreateProblem cannot exercise -- variable/row names, objective scaling factor, + * and the quadratic-presence flags -- in addition to the numeric arrays and the CSC matrix. + */ +void verify_attributes_against_parser(const std::string& path) +{ + namespace mo = cuopt::mathematical_optimization; + + const mo::io::mps_data_model_t model = mo::io::read(path); + + cuOptOptimizationProblem problem = nullptr; + ASSERT_EQ(cuOptReadProblem(path.c_str(), &problem), CUOPT_SUCCESS); + + const int nv = model.get_n_variables(); + const int nc = model.get_n_constraints(); + const int nnz = static_cast(model.get_constraint_matrix_values().size()); + + int v = 0; + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &v), CUOPT_SUCCESS); + EXPECT_EQ(v, nv); + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &v), CUOPT_SUCCESS); + EXPECT_EQ(v, nc); + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &v), CUOPT_SUCCESS); + EXPECT_EQ(v, nnz); + + /* num_integers / is_mip derived from the model's variable types (normalized). */ + int expect_integers = 0; + int expect_discrete = 0; + for (char c : model.get_variable_types()) { + const mo::var_t t = mo::char_to_var_type(c); + if (t == mo::var_t::INTEGER) { ++expect_integers; } + if (t == mo::var_t::INTEGER || t == mo::var_t::SEMI_CONTINUOUS) { ++expect_discrete; } + } + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &v), CUOPT_SUCCESS); + EXPECT_EQ(v, expect_integers); + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &v), CUOPT_SUCCESS); + EXPECT_EQ(v, expect_discrete > 0 ? 1 : 0); + + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_OBJECTIVE_SENSE, &v), CUOPT_SUCCESS); + EXPECT_EQ(v, model.get_sense() ? CUOPT_MAXIMIZE : CUOPT_MINIMIZE); + + int qobj = 0, qcon = 0; + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &qobj), + CUOPT_SUCCESS); + EXPECT_EQ(qobj, model.has_quadratic_objective() ? 1 : 0); + EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &qcon), + CUOPT_SUCCESS); + EXPECT_EQ(qcon, model.has_quadratic_constraints() ? 1 : 0); + + double d = 0.0; + EXPECT_EQ(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &d), CUOPT_SUCCESS); + EXPECT_DOUBLE_EQ(d, model.get_objective_offset()); + EXPECT_EQ(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &d), + CUOPT_SUCCESS); + EXPECT_DOUBLE_EQ(d, model.get_objective_scaling_factor()); + + /* Float arrays: compare element-wise against the parsed model. */ + auto check_float_array = [&](cuopt_int_t attr, const std::vector& expected, int count) { + if (expected.empty()) { return; } + ASSERT_EQ(static_cast(expected.size()), count); + std::vector got(static_cast(count)); + ASSERT_EQ(cuOptGetProblemFloatArrayAttribute(problem, attr, got.data(), count), CUOPT_SUCCESS); + for (int i = 0; i < count; ++i) { + EXPECT_DOUBLE_EQ(got[i], expected[i]); + } + }; + check_float_array( + CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, model.get_objective_coefficients(), nv); + check_float_array(CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, model.get_variable_lower_bounds(), nv); + check_float_array(CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, model.get_variable_upper_bounds(), nv); + check_float_array(CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, model.get_constraint_bounds(), nc); + + /* Char arrays: constraint sense passes through; variable types are normalized to 'C'/'I'/'S'. */ + const std::vector& mrt = model.get_row_types(); + if (!mrt.empty()) { + ASSERT_EQ(static_cast(mrt.size()), nc); + std::vector got(static_cast(nc)); + ASSERT_EQ( + cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, got.data(), nc), + CUOPT_SUCCESS); + for (int i = 0; i < nc; ++i) { + EXPECT_EQ(got[i], mrt[i]); + } + } + const std::vector& mvt = model.get_variable_types(); + if (!mvt.empty()) { + ASSERT_EQ(static_cast(mvt.size()), nv); + std::vector got(static_cast(nv)); + ASSERT_EQ( + cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, got.data(), nv), + CUOPT_SUCCESS); + for (int i = 0; i < nv; ++i) { + EXPECT_EQ(got[i], mo::var_type_to_char(mo::char_to_var_type(mvt[i]))); + } + } + + /* String arrays: the key capability with no dedicated getter -- compare names exactly. */ + const std::vector& vnames = model.get_variable_names(); + if (!vnames.empty()) { + ASSERT_EQ(static_cast(vnames.size()), nv); + std::vector got(static_cast(nv)); + ASSERT_EQ(cuOptGetProblemStringArrayAttribute( + problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, got.data(), nv), + CUOPT_SUCCESS); + for (int i = 0; i < nv; ++i) { + EXPECT_EQ(std::string(got[i]), vnames[i]); + } + } + const std::vector& rnames = model.get_row_names(); + if (!rnames.empty()) { + ASSERT_EQ(static_cast(rnames.size()), nc); + std::vector got(static_cast(nc)); + ASSERT_EQ( + cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_ROW_NAMES, got.data(), nc), + CUOPT_SUCCESS); + for (int i = 0; i < nc; ++i) { + EXPECT_EQ(std::string(got[i]), rnames[i]); + } + } + + /* CSC values: must be the transpose of the model's CSR (compare as (row,col,value) multisets). */ + if (nnz > 0) { + const std::vector& moff = model.get_constraint_matrix_offsets(); + const std::vector& mind = model.get_constraint_matrix_indices(); + const std::vector& mval = model.get_constraint_matrix_values(); + std::vector> expected; + expected.reserve(static_cast(nnz)); + for (int i = 0; i < nc; ++i) { + for (int k = moff[i]; k < moff[i + 1]; ++k) { + expected.emplace_back(i, mind[k], mval[k]); + } + } + std::vector csc_off(static_cast(nv + 1)); + std::vector csc_row(static_cast(nnz)); + std::vector csc_val(static_cast(nnz)); + ASSERT_EQ(cuOptGetConstraintMatrixCSC(problem, csc_off.data(), csc_row.data(), csc_val.data()), + CUOPT_SUCCESS); + std::vector> got; + got.reserve(static_cast(nnz)); + for (int c = 0; c < nv; ++c) { + for (int k = csc_off[c]; k < csc_off[c + 1]; ++k) { + got.emplace_back(csc_row[k], c, csc_val[k]); + } + } + std::sort(expected.begin(), expected.end()); + std::sort(got.begin(), got.end()); + EXPECT_EQ(expected, got); + } + + cuOptDestroyProblem(&problem); +} + +} // namespace + TEST(c_api, problem_attributes) { const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); @@ -787,6 +952,24 @@ TEST(c_api, problem_attributes) EXPECT_EQ(test_problem_attributes(filename.c_str()), CUOPT_SUCCESS); } +TEST(c_api, problem_attributes_vs_parser_lp) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + verify_attributes_against_parser(rapidsDatasetRootDir + "/linear_programming/afiro_original.mps"); +} + +TEST(c_api, problem_attributes_vs_parser_mip) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + verify_attributes_against_parser(rapidsDatasetRootDir + "/mip/50v-10.mps"); +} + +TEST(c_api, problem_attributes_vs_parser_qp) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + verify_attributes_against_parser(rapidsDatasetRootDir + "/quadratic_programming/QP_Test_1.qps"); +} + TEST(c_api, problem_attributes_mip) { const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); @@ -801,5 +984,10 @@ TEST(c_api, problem_attributes_qp) EXPECT_EQ(test_problem_attributes_qp(filename.c_str()), CUOPT_SUCCESS); } +TEST(c_api, problem_attributes_created) +{ + EXPECT_EQ(test_problem_attributes_created(), CUOPT_SUCCESS); +} + // Note: cuopt_cli subprocess tests are in Python (test_cpu_only_execution.py) // which provides better cross-platform subprocess handling 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 580c92a8a8..7b38d95f5f 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 @@ -91,6 +91,9 @@ cuopt_int_t test_gpu_problem_remote_after_create(const char* filename); cuopt_int_t test_problem_attributes(const char* filename); cuopt_int_t test_problem_attributes_mip(const char* filename); cuopt_int_t test_problem_attributes_qp(const char* filename); +/* Build a known problem with cuOptCreateProblem and verify getters against the constructed values, + * covering attributes that have no dedicated getter to cross-check against. */ +cuopt_int_t test_problem_attributes_created(void); #ifdef __cplusplus } From e6bb84975748f59d5bdc3635ad9ef9d9d6e58d25 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Mon, 20 Jul 2026 14:17:09 -0400 Subject: [PATCH 3/6] C API extensions implement feedback after review * remove cosmetic static casts in new fuctions * move attribute helpers into cuopt_c.cpp instead of a new file * add cuOptGetConstraintMatrixCSR and deprecate cuOptGetConstraintMatrix * revise/remove comments --- .../mathematical_optimization/constants.h | 7 +- .../cuopt/mathematical_optimization/cuopt_c.h | 50 ++- cpp/src/pdlp/CMakeLists.txt | 1 - cpp/src/pdlp/cuopt_c.cpp | 324 +++++++++++++++++- cpp/src/pdlp/cuopt_c_attributes.cpp | 285 --------------- 5 files changed, 344 insertions(+), 323 deletions(-) delete mode 100644 cpp/src/pdlp/cuopt_c_attributes.cpp diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index db93b88bd4..a425f6d70b 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -238,15 +238,10 @@ #define CUOPT_ATTR_IS_MIP 8 #define CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE 9 #define CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS 10 -/* NOTE: HAS_QUADRATIC_OBJECTIVE / HAS_QUADRATIC_CONSTRAINTS report presence only. The quadratic - * objective matrix (Q) and quadratic constraint data are not currently retrievable through any - * getter */ /* @brief Numeric/char array problem attribute selectors * (see cuOptGetProblem{Float,Char}ArrayAttribute; sized by num_variables / num_constraints). - * The constraint matrix is retrieved via cuOptGetConstraintMatrix / cuOptGetConstraintMatrixCSC, - * not through these selectors. Passed as cuopt_int_t. Numbered in a separate range from the scalar - * selectors so a selector from the wrong family fails validation (there is no compile-time type). + * 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 diff --git a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h index 0aab0adf76..0161ca35a9 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -564,8 +564,11 @@ cuopt_int_t cuOptGetObjectiveCoefficients(cuOptOptimizationProblem problem, cuopt_int_t cuOptGetNumNonZeros(cuOptOptimizationProblem problem, cuopt_int_t* num_non_zeros_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; quadratic constraint terms (if any) - * are not included. + * format. This is the matrix of the linear constraints only. + * + * @deprecated Use cuOptGetConstraintMatrixCSR (identical CSR output) or + * cuOptGetConstraintMatrixCSC. This function forwards to cuOptGetConstraintMatrixCSR and logs a + * deprecation warning at runtime. * * @param[in] problem - The optimization problem. * @@ -588,15 +591,39 @@ 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 in compressed sparse row (CSR) format. + * + * This is the canonical CSR getter; it pairs symmetrically with cuOptGetConstraintMatrixCSC. The + * deprecated cuOptGetConstraintMatrix forwards here. + * + * @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; quadratic constraint terms (if any) - * are not included. + * 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 cuOptGetNumVariables) that on output will contain the column offsets - * of the constraint matrix. + * 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 @@ -1093,7 +1120,7 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ * 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 - * cuOptGetNumVariables / cuOptGetNumConstraints). The sole exception to copy-out is the + * 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. @@ -1101,9 +1128,8 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ * The constraint matrix is retrieved via cuOptGetConstraintMatrix (CSR) / * cuOptGetConstraintMatrixCSC. * - * TODO: quadratic data is not yet retrievable. CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE and - * CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS report presence only; there is no getter for the - * quadratic objective matrix (Q) or the quadratic constraint rows. + * 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). */ @@ -1118,7 +1144,7 @@ cuopt_int_t cuOptGetProblemFloatAttribute(cuOptOptimizationProblem problem, /** @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 - * cuOptGetNumVariables / cuOptGetNumConstraints). */ + * cuOptGetProblemIntAttribute). */ cuopt_int_t cuOptGetProblemFloatArrayAttribute(cuOptOptimizationProblem problem, cuopt_int_t attribute, cuopt_float_t* out, @@ -1126,7 +1152,7 @@ cuopt_int_t cuOptGetProblemFloatArrayAttribute(cuOptOptimizationProblem problem, /** @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 - * cuOptGetNumConstraints / cuOptGetNumVariables). */ + * cuOptGetProblemIntAttribute). */ cuopt_int_t cuOptGetProblemCharArrayAttribute(cuOptOptimizationProblem problem, cuopt_int_t attribute, char* out, diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 38e140341d..f5f26837b6 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -35,7 +35,6 @@ set(LP_CORE_FILES set(LP_ADAPTER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/cython_solve.cu ${CMAKE_CURRENT_SOURCE_DIR}/cuopt_c.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cuopt_c_attributes.cpp ) # Choose which files to include based on build mode diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 5d987c1352..35ddf52591 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -21,6 +21,7 @@ #include +#include #include #include #include @@ -30,11 +31,13 @@ using cuopt::mathematical_optimization::char_to_var_type; 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 +95,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 +242,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 +792,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_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 +1358,226 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ return CUOPT_INVALID_ARGUMENT; } } + +/* -------------------------------------------------------------------------- */ +/* Generic problem attribute getters (declared in cuopt_c.h) */ +/* -------------------------------------------------------------------------- */ + +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(); + + const std::vector row_offsets = iface->get_constraint_matrix_offsets_host(); + const std::vector col_indices = iface->get_constraint_matrix_indices_host(); + const 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) { + for (cuopt_int_t c = 0; c <= n; ++c) { + column_offsets_ptr[c] = 0; + } + return CUOPT_SUCCESS; + } + if (row_indices_ptr == nullptr || values_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + + // Count non-zeros per column, then prefix-sum into column offsets. + std::vector col_counts(n, 0); + for (cuopt_int_t k = 0; k < nnz; ++k) { + const cuopt_int_t c = col_indices[k]; + if (c < 0 || c >= n) { return CUOPT_VALIDATION_ERROR; } + ++col_counts[c]; + } + column_offsets_ptr[0] = 0; + for (cuopt_int_t c = 0; c < n; ++c) { + column_offsets_ptr[c + 1] = column_offsets_ptr[c] + col_counts[c]; + } + + // Scatter each CSR entry into its CSC position using a running write cursor per column. + std::vector next(column_offsets_ptr, column_offsets_ptr + n); + for (cuopt_int_t i = 0; i < m; ++i) { + const cuopt_int_t row_begin = row_offsets[i]; + const cuopt_int_t row_end = row_offsets[i + 1]; + for (cuopt_int_t k = row_begin; k < row_end; ++k) { + const cuopt_int_t c = col_indices[k]; + const cuopt_int_t dest = next[c]++; + row_indices_ptr[dest] = i; + values_ptr[dest] = values[k]; + } + } + return CUOPT_SUCCESS; +} diff --git a/cpp/src/pdlp/cuopt_c_attributes.cpp b/cpp/src/pdlp/cuopt_c_attributes.cpp deleted file mode 100644 index 587a68c71e..0000000000 --- a/cpp/src/pdlp/cuopt_c_attributes.cpp +++ /dev/null @@ -1,285 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include -#include -#include -#include - -#include -#include -#include - -using namespace cuopt::mathematical_optimization; - -namespace { - -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; - } -} - -} // namespace - -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 (static_cast(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 (static_cast(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 (static_cast(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[static_cast(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 (count != static_cast(names.size())) { return CUOPT_INVALID_ARGUMENT; } - for (cuopt_int_t i = 0; i < count; ++i) { - strings_out[i] = names[static_cast(i)].c_str(); - } - 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(); - - const std::vector row_offsets = iface->get_constraint_matrix_offsets_host(); - const std::vector col_indices = iface->get_constraint_matrix_indices_host(); - const 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() < static_cast(m + 1) || nnz == 0) { - for (cuopt_int_t c = 0; c <= n; ++c) { - column_offsets_ptr[c] = 0; - } - return CUOPT_SUCCESS; - } - if (row_indices_ptr == nullptr || values_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } - - // Count non-zeros per column, then prefix-sum into column offsets. - std::vector col_counts(static_cast(n), 0); - for (cuopt_int_t k = 0; k < nnz; ++k) { - const cuopt_int_t c = col_indices[static_cast(k)]; - if (c < 0 || c >= n) { return CUOPT_VALIDATION_ERROR; } - ++col_counts[static_cast(c)]; - } - column_offsets_ptr[0] = 0; - for (cuopt_int_t c = 0; c < n; ++c) { - column_offsets_ptr[c + 1] = column_offsets_ptr[c] + col_counts[static_cast(c)]; - } - - // Scatter each CSR entry into its CSC position using a running write cursor per column. - std::vector next(column_offsets_ptr, column_offsets_ptr + n); - for (cuopt_int_t i = 0; i < m; ++i) { - const cuopt_int_t row_begin = row_offsets[static_cast(i)]; - const cuopt_int_t row_end = row_offsets[static_cast(i + 1)]; - for (cuopt_int_t k = row_begin; k < row_end; ++k) { - const cuopt_int_t c = col_indices[static_cast(k)]; - const cuopt_int_t dest = next[static_cast(c)]++; - row_indices_ptr[dest] = i; - values_ptr[dest] = values[static_cast(k)]; - } - } - return CUOPT_SUCCESS; -} From 97f9c99042719713d8b52fcd325c0b59f5ab441c Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Mon, 20 Jul 2026 15:49:00 -0400 Subject: [PATCH 4/6] modify tests for extended C API to use create functions Use the create functions with direct comparison of values returned from getters wherever possible. Use mps read for checking getters of names. --- .../c_api_tests/c_api_test.c | 920 ++++++++---------- .../c_api_tests/c_api_tests.cpp | 210 +--- .../c_api_tests/c_api_tests.h | 14 +- 3 files changed, 447 insertions(+), 697 deletions(-) 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 9a718280c2..194637a0d4 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 @@ -3167,19 +3167,6 @@ cuopt_int_t test_gpu_problem_remote_after_create(const char* filename) return status; } -/* Key scalar facts filled by read_all_problem_attributes(), used by category-specific tests. */ -typedef struct { - cuopt_int_t n_variables; - cuopt_int_t n_constraints; - cuopt_int_t n_nonzeros; - cuopt_int_t n_integers; - cuopt_int_t category; - cuopt_int_t is_mip; - cuopt_int_t has_quadratic_objective; - cuopt_int_t has_quadratic_constraints; - cuopt_int_t num_integer_type_chars; /* count of 'I' in variable_types */ -} problem_facts_t; - static int doubles_equal(const cuopt_float_t* a, const cuopt_float_t* b, cuopt_int_t n) { cuopt_int_t i; @@ -3189,6 +3176,32 @@ static int doubles_equal(const cuopt_float_t* a, const cuopt_float_t* b, cuopt_i return 1; } +static int ints_equal(const cuopt_int_t* a, const cuopt_int_t* b, cuopt_int_t n) +{ + cuopt_int_t i; + for (i = 0; i < n; ++i) { + if (a[i] != b[i]) { return 0; } + } + return 1; +} + +/* Report a failing C API call. Returns the status so callers can do: + * status = check_ok(cuOptSomeCall(...), "what"); if (status != CUOPT_SUCCESS) goto DONE; */ +static cuopt_int_t check_ok(cuopt_int_t status, const char* what) +{ + if (status != CUOPT_SUCCESS) { printf("FAILED (%d): %s\n", (int)status, what); } + return status; +} + +/* Report a failing ground-truth comparison. Returns CUOPT_SUCCESS when cond holds, otherwise + * CUOPT_VALIDATION_ERROR, so callers can do: + * status = check_true(got == expected, "what"); if (status != CUOPT_SUCCESS) goto DONE; */ +static cuopt_int_t check_true(int cond, const char* what) +{ + if (!cond) { printf("ground-truth failure: %s\n", what); } + return cond ? CUOPT_SUCCESS : CUOPT_VALIDATION_ERROR; +} + /* One sparse matrix entry, used to compare CSR and CSC as an unordered (row,col,value) multiset. */ typedef struct { cuopt_int_t row; @@ -3208,364 +3221,14 @@ static int compare_coo(const void* a, const void* b) } /* - * Read back every attribute getter and cross-check it against the dedicated accessor / the other - * matrix layout. Fills *facts on success. Returns CUOPT_SUCCESS or the first failing status. - */ -static cuopt_int_t read_all_problem_attributes(const char* filename, problem_facts_t* facts) -{ -#define ATTR_CHECK(call) \ - do { \ - cuopt_int_t _s = (call); \ - if (_s != CUOPT_SUCCESS) { \ - printf("FAILED (%d): %s\n", (int)_s, #call); \ - status = _s; \ - goto DONE; \ - } \ - } while (0) - - cuOptOptimizationProblem problem = NULL; - cuopt_int_t status = CUOPT_SUCCESS; - - cuopt_float_t* obj_g = NULL; /* _g = via generic attribute getter */ - cuopt_float_t* obj_d = NULL; /* _d = via dedicated getter */ - cuopt_float_t* vlb_g = NULL; - cuopt_float_t* vlb_d = NULL; - cuopt_float_t* vub_g = NULL; - cuopt_float_t* vub_d = NULL; - cuopt_float_t* rhs_g = NULL; - cuopt_float_t* rhs_d = NULL; - cuopt_float_t* clb_g = NULL; - cuopt_float_t* cub_g = NULL; - char* sense_g = NULL; - char* sense_d = NULL; - char* types_g = NULL; - char* types_d = NULL; - const char** var_names = NULL; - const char** row_names = NULL; - cuopt_int_t* csr_off = NULL; - cuopt_int_t* csr_col = NULL; - cuopt_float_t* csr_val = NULL; - cuopt_int_t* csc_off = NULL; - cuopt_int_t* csc_row = NULL; - cuopt_float_t* csc_val = NULL; - cuopt_int_t* col_counts = NULL; - coo_triple_t* csr_triples = NULL; - coo_triple_t* csc_triples = NULL; - - cuopt_int_t nv = 0, nc = 0, nnz = 0; - cuopt_int_t a_nv = 0, a_nc = 0, a_nnz = 0, a_sense = 0, d_sense = 0; - cuopt_float_t f_offset = 0.0, f_scale = 0.0, d_offset = 0.0; - cuopt_int_t i = 0, k = 0; - - ATTR_CHECK(cuOptReadProblem(filename, &problem)); - - /* --- dimensions (used to size all arrays) --- */ - ATTR_CHECK(cuOptGetNumVariables(problem, &nv)); - ATTR_CHECK(cuOptGetNumConstraints(problem, &nc)); - ATTR_CHECK(cuOptGetNumNonZeros(problem, &nnz)); - - /* --- scalar int attributes, cross-checked against dedicated getters where they exist --- */ - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &a_nv)); - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &a_nc)); - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &a_nnz)); - if (a_nv != nv || a_nc != nc || a_nnz != nnz) { - printf("Scalar dimension attribute mismatch\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &facts->n_integers)); - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_PROBLEM_CATEGORY, &facts->category)); - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &facts->is_mip)); - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, - &facts->has_quadratic_objective)); - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, - &facts->has_quadratic_constraints)); - ATTR_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_OBJECTIVE_SENSE, &a_sense)); - ATTR_CHECK(cuOptGetObjectiveSense(problem, &d_sense)); - if (a_sense != d_sense) { - printf("Objective sense mismatch (generic %d vs dedicated %d)\n", (int)a_sense, (int)d_sense); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - - /* --- scalar float attributes --- */ - ATTR_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &f_offset)); - ATTR_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &f_scale)); - ATTR_CHECK(cuOptGetObjectiveOffset(problem, &d_offset)); - if (f_offset != d_offset) { - printf("Objective offset mismatch (generic %g vs dedicated %g)\n", f_offset, d_offset); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - (void)f_scale; - - /* --- float arrays: generic getter must match dedicated getter byte-for-byte --- */ - obj_g = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); - obj_d = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); - vlb_g = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); - vlb_d = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); - vub_g = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); - vub_d = (cuopt_float_t*)malloc((size_t)nv * sizeof(cuopt_float_t)); - rhs_g = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); - rhs_d = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); - if (!obj_g || !obj_d || !vlb_g || !vlb_d || !vub_g || !vub_d || !rhs_g || !rhs_d) { - status = CUOPT_OUT_OF_MEMORY; - goto DONE; - } - ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, - obj_g, nv)); - ATTR_CHECK(cuOptGetObjectiveCoefficients(problem, obj_d)); - ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, - vlb_g, nv)); - ATTR_CHECK(cuOptGetVariableLowerBounds(problem, vlb_d)); - ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, - vub_g, nv)); - ATTR_CHECK(cuOptGetVariableUpperBounds(problem, vub_d)); - ATTR_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, rhs_g, nc)); - ATTR_CHECK(cuOptGetConstraintRightHandSide(problem, rhs_d)); - if (!doubles_equal(obj_g, obj_d, nv) || !doubles_equal(vlb_g, vlb_d, nv) || - !doubles_equal(vub_g, vub_d, nv) || !doubles_equal(rhs_g, rhs_d, nc)) { - printf("Generic float array getter disagrees with dedicated getter\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - - /* --- constraint lower/upper bounds: optional (ranged models only). Exercise the code path; - accept SUCCESS (populated) or CUOPT_VALIDATION_ERROR (empty for a sense+rhs model). --- */ - clb_g = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); - cub_g = (cuopt_float_t*)malloc((size_t)nc * sizeof(cuopt_float_t)); - if (!clb_g || !cub_g) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } - { - cuopt_int_t s_lb = - cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS, clb_g, nc); - cuopt_int_t s_ub = - cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS, cub_g, nc); - if ((s_lb != CUOPT_SUCCESS && s_lb != CUOPT_VALIDATION_ERROR) || - (s_ub != CUOPT_SUCCESS && s_ub != CUOPT_VALIDATION_ERROR)) { - printf("Constraint bound getters returned unexpected status (%d, %d)\n", (int)s_lb, (int)s_ub); - status = (s_lb != CUOPT_SUCCESS && s_lb != CUOPT_VALIDATION_ERROR) ? s_lb : s_ub; - goto DONE; - } - } - - /* --- char arrays: generic must match dedicated --- */ - sense_g = (char*)malloc((size_t)nc * sizeof(char)); - sense_d = (char*)malloc((size_t)nc * sizeof(char)); - types_g = (char*)malloc((size_t)nv * sizeof(char)); - types_d = (char*)malloc((size_t)nv * sizeof(char)); - if (!sense_g || !sense_d || !types_g || !types_d) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } - ATTR_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, sense_g, - nc)); - ATTR_CHECK(cuOptGetConstraintSense(problem, sense_d)); - ATTR_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, types_g, - nv)); - ATTR_CHECK(cuOptGetVariableTypes(problem, types_d)); - for (i = 0; i < nc; ++i) { - if (sense_g[i] != sense_d[i]) { - printf("Constraint sense mismatch at %d\n", (int)i); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - } - facts->num_integer_type_chars = 0; - for (i = 0; i < nv; ++i) { - if (types_g[i] != types_d[i]) { - printf("Variable type mismatch at %d\n", (int)i); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - if (types_g[i] == CUOPT_INTEGER) { facts->num_integer_type_chars++; } - } - - /* --- string arrays: borrowed pointers into cuOpt storage --- */ - var_names = (const char**)malloc((size_t)nv * sizeof(const char*)); - row_names = (const char**)malloc((size_t)nc * sizeof(const char*)); - if (!var_names || !row_names) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } - ATTR_CHECK(cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, - var_names, nv)); - ATTR_CHECK(cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_ROW_NAMES, row_names, - nc)); - if (nv > 0 && var_names[0] == NULL) { - printf("Expected non-null variable name pointers\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - if (nc > 0 && row_names[0] == NULL) { - printf("Expected non-null row name pointers\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - - /* --- constraint matrix: CSR (dedicated) and CSC (new), cross-checked --- */ - csr_off = (cuopt_int_t*)malloc((size_t)(nc + 1) * sizeof(cuopt_int_t)); - csr_col = (cuopt_int_t*)malloc((size_t)nnz * sizeof(cuopt_int_t)); - csr_val = (cuopt_float_t*)malloc((size_t)nnz * sizeof(cuopt_float_t)); - csc_off = (cuopt_int_t*)malloc((size_t)(nv + 1) * sizeof(cuopt_int_t)); - csc_row = (cuopt_int_t*)malloc((size_t)nnz * sizeof(cuopt_int_t)); - csc_val = (cuopt_float_t*)malloc((size_t)nnz * sizeof(cuopt_float_t)); - col_counts = (cuopt_int_t*)calloc((size_t)(nv > 0 ? nv : 1), sizeof(cuopt_int_t)); - if (!csr_off || !csr_col || !csr_val || !csc_off || !csc_row || !csc_val || !col_counts) { - status = CUOPT_OUT_OF_MEMORY; - goto DONE; - } - ATTR_CHECK(cuOptGetConstraintMatrix(problem, csr_off, csr_col, csr_val)); - ATTR_CHECK(cuOptGetConstraintMatrixCSC(problem, csc_off, csc_row, csc_val)); - if (csr_off[nc] != nnz || csc_off[nv] != nnz || csr_off[0] != 0 || csc_off[0] != 0) { - printf("CSR/CSC offsets inconsistent with nnz\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - /* Independent transpose check: per-column nnz from CSR column indices must equal the gaps in - the CSC column offsets. */ - for (k = 0; k < nnz; ++k) { - if (csr_col[k] < 0 || csr_col[k] >= nv) { - printf("CSR column index out of range\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - col_counts[csr_col[k]]++; - } - for (i = 0; i < nv; ++i) { - if (csc_off[i + 1] - csc_off[i] != col_counts[i]) { - printf("CSC column %d count disagrees with CSR transpose\n", (int)i); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - } - - /* Full equivalence: CSR and CSC must encode the same (row, col, value) multiset. Expand each - layout to COO triples, sort both, and compare entry-by-entry (values included). This catches - mispaired values / wrong row indices that the per-column count check alone would miss. */ - csr_triples = (coo_triple_t*)malloc((size_t)nnz * sizeof(coo_triple_t)); - csc_triples = (coo_triple_t*)malloc((size_t)nnz * sizeof(coo_triple_t)); - if (!csr_triples || !csc_triples) { status = CUOPT_OUT_OF_MEMORY; goto DONE; } - for (i = 0; i < nc; ++i) { - for (k = csr_off[i]; k < csr_off[i + 1]; ++k) { - csr_triples[k].row = i; - csr_triples[k].col = csr_col[k]; - csr_triples[k].val = csr_val[k]; - } - } - for (i = 0; i < nv; ++i) { - for (k = csc_off[i]; k < csc_off[i + 1]; ++k) { - csc_triples[k].row = csc_row[k]; - csc_triples[k].col = i; - csc_triples[k].val = csc_val[k]; - } - } - qsort(csr_triples, (size_t)nnz, sizeof(coo_triple_t), compare_coo); - qsort(csc_triples, (size_t)nnz, sizeof(coo_triple_t), compare_coo); - for (k = 0; k < nnz; ++k) { - if (csr_triples[k].row != csc_triples[k].row || csr_triples[k].col != csc_triples[k].col || - csr_triples[k].val != csc_triples[k].val) { - printf("CSC entry %d disagrees with CSR (row/col/value)\n", (int)k); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - } - - facts->n_variables = nv; - facts->n_constraints = nc; - facts->n_nonzeros = nnz; - -DONE: - cuOptDestroyProblem(&problem); - free(obj_g); - free(obj_d); - free(vlb_g); - free(vlb_d); - free(vub_g); - free(vub_d); - free(rhs_g); - free(rhs_d); - free(clb_g); - free(cub_g); - free(sense_g); - free(sense_d); - free(types_g); - free(types_d); - free(var_names); - free(row_names); - free(csr_off); - free(csr_col); - free(csr_val); - free(csc_off); - free(csc_row); - free(csc_val); - free(col_counts); - free(csr_triples); - free(csc_triples); - return status; -#undef ATTR_CHECK -} - -/* LP entry point: read back every getter and verify internal consistency. */ -cuopt_int_t test_problem_attributes(const char* filename) -{ - problem_facts_t facts = {0}; - return read_all_problem_attributes(filename, &facts); -} - -/* MIP/IP entry point: full read plus MIP-specific expectations. */ -cuopt_int_t test_problem_attributes_mip(const char* filename) -{ - problem_facts_t facts = {0}; - cuopt_int_t status = read_all_problem_attributes(filename, &facts); - if (status != CUOPT_SUCCESS) { return status; } - - if (facts.is_mip != 1) { - printf("Expected is_mip=1 for MIP file\n"); - return CUOPT_VALIDATION_ERROR; - } - /* category is MIP (1) or IP (2). */ - if (facts.category != 1 && facts.category != 2) { - printf("Expected MIP/IP category, got %d\n", (int)facts.category); - return CUOPT_VALIDATION_ERROR; - } - if (facts.n_integers <= 0) { - printf("Expected num_integers > 0 for MIP file\n"); - return CUOPT_VALIDATION_ERROR; - } - if (facts.num_integer_type_chars <= 0) { - printf("Expected at least one integer variable type\n"); - return CUOPT_VALIDATION_ERROR; - } - return CUOPT_SUCCESS; -} - -/* QP entry point: full read plus quadratic-objective expectation. */ -cuopt_int_t test_problem_attributes_qp(const char* filename) -{ - problem_facts_t facts = {0}; - cuopt_int_t status = read_all_problem_attributes(filename, &facts); - if (status != CUOPT_SUCCESS) { return status; } - - if (facts.has_quadratic_objective != 1) { - printf("Expected has_quadratic_objective=1 for QP file\n"); - return CUOPT_VALIDATION_ERROR; - } - return CUOPT_SUCCESS; -} - -/* - * Build a small MIP by hand with cuOptCreateProblem, then read it back through the new getters and - * compare against the exact values we constructed. This is the only path that value-checks the - * attributes with no dedicated getter to cross-check (num_integers, problem_category, is_mip, - * has_quadratic_*, objective_scaling_factor) and the CSC matrix values. Needs no dataset file and - * works on either backend. + * Build a small mixed-integer LP by hand with cuOptCreateProblem, then read every getter back and + * compare against the exact values we constructed. Ground truth comes from the create call itself, + * so there is no dependence on a parser or a dataset file, and it works on either backend. This + * covers every attribute cuOptCreateProblem can set; the ranged-only, quadratic-only, and name-only + * attributes are covered by test_problem_attributes_ranged, _quadratic, and _names respectively. */ cuopt_int_t test_problem_attributes_created(void) { -#define C_CHECK(call) \ - do { \ - cuopt_int_t _s = (call); \ - if (_s != CUOPT_SUCCESS) { \ - printf("FAILED (%d): %s\n", (int)_s, #call); \ - status = _s; \ - goto DONE; \ - } \ - } while (0) - cuOptOptimizationProblem problem = NULL; cuopt_int_t status = CUOPT_SUCCESS; @@ -3591,6 +3254,9 @@ cuopt_int_t test_problem_attributes_created(void) char cbuf[3]; cuopt_int_t ival = 0; cuopt_float_t fval = 0.0; + cuopt_int_t csr_off[3]; + cuopt_int_t csr_col[4]; + cuopt_float_t csr_val[4]; cuopt_int_t csc_off[4]; cuopt_int_t csc_row[4]; cuopt_float_t csc_val[4]; @@ -3599,120 +3265,135 @@ cuopt_int_t test_problem_attributes_created(void) cuopt_int_t i = 0, k = 0; const char* names_probe[3]; - C_CHECK(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)); - - /* --- scalar attributes with NO dedicated getter: verify against the known construction --- */ - C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &ival)); - if (ival != 2) { - printf("num_integers: expected 2, got %d\n", (int)ival); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_PROBLEM_CATEGORY, &ival)); - if (ival != 1 /* MIP */) { - printf("problem_category: expected MIP(1), got %d\n", (int)ival); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &ival)); - if (ival != 1) { - printf("is_mip: expected 1, got %d\n", (int)ival); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival)); - if (ival != 0) { - printf("has_quadratic_objective: expected 0, got %d\n", (int)ival); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival)); - if (ival != 0) { - printf("has_quadratic_constraints: expected 0, got %d\n", (int)ival); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &fval)); - if (fval != 1.0) { /* create does not set it; default is 1 */ - printf("objective_scaling_factor: expected 1, got %g\n", fval); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &fval)); - if (fval != objective_offset) { - printf("objective_offset: expected %g, got %g\n", objective_offset, fval); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } + status = check_ok(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), + "cuOptCreateProblem"); + if (status != CUOPT_SUCCESS) goto DONE; + + /* --- dimensions and objective sense: generic getters vs the construction --- */ + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &ival), + "get num_variables"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == num_variables, "num_variables"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &ival), + "get num_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == num_constraints, "num_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &ival), + "get num_nonzeros"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == nnz, "num_nonzeros"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_OBJECTIVE_SENSE, &ival), + "get objective_sense"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == CUOPT_MINIMIZE, "objective_sense"); + if (status != CUOPT_SUCCESS) goto DONE; + + /* --- scalar attributes with no dedicated getter: verify against the known construction --- */ + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &ival), + "get num_integers"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 2, "num_integers"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_PROBLEM_CATEGORY, &ival), + "get problem_category"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 1 /* MIP */, "problem_category"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &ival), "get is_mip"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 1, "is_mip"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival), + "get has_quadratic_objective"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 0, "has_quadratic_objective"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival), + "get has_quadratic_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 0, "has_quadratic_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &fval), + "get objective_scaling_factor"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(fval == 1.0, "objective_scaling_factor"); /* create leaves the default of 1 */ + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &fval), + "get objective_offset"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(fval == objective_offset, "objective_offset"); + if (status != CUOPT_SUCCESS) goto DONE; /* --- array getters: compare against the exact values we constructed with --- */ - C_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, fbuf, - num_variables)); - if (!doubles_equal(fbuf, objective_coefficients, num_variables)) { - printf("objective_coefficients mismatch after create\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, fbuf, - num_variables)); - if (!doubles_equal(fbuf, lower_bounds, num_variables)) { - printf("variable lower bounds mismatch after create\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, fbuf, - num_variables)); - if (!doubles_equal(fbuf, upper_bounds, num_variables)) { - printf("variable upper bounds mismatch after create\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK( - cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, fbuf, num_constraints)); - if (!doubles_equal(fbuf, rhs, num_constraints)) { - printf("rhs mismatch after create\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - C_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, cbuf, - num_constraints)); - for (i = 0; i < num_constraints; ++i) { - if (cbuf[i] != constraint_sense[i]) { - printf("constraint sense mismatch at %d after create\n", (int)i); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - } - C_CHECK(cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, cbuf, - num_variables)); - for (i = 0; i < num_variables; ++i) { - if (cbuf[i] != variable_types[i]) { - printf("variable type mismatch at %d after create\n", (int)i); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } - } + status = check_ok(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, fbuf, num_variables), + "get objective_coefficients"); + if (status != CUOPT_SUCCESS) goto DONE; + status = + check_true(doubles_equal(fbuf, objective_coefficients, num_variables), "objective_coefficients"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, fbuf, num_variables), + "get variable_lower_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(doubles_equal(fbuf, lower_bounds, num_variables), "variable_lower_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, fbuf, num_variables), + "get variable_upper_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(doubles_equal(fbuf, upper_bounds, num_variables), "variable_upper_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok( + cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, fbuf, num_constraints), + "get constraint_rhs"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(doubles_equal(fbuf, rhs, num_constraints), "constraint_rhs"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok( + cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, cbuf, num_constraints), + "get constraint_sense"); + if (status != CUOPT_SUCCESS) goto DONE; + status = + check_true(memcmp(cbuf, constraint_sense, (size_t)num_constraints) == 0, "constraint_sense"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok( + cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, cbuf, num_variables), + "get variable_types"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(memcmp(cbuf, variable_types, (size_t)num_variables) == 0, "variable_types"); + if (status != CUOPT_SUCCESS) goto DONE; + + /* --- CSR values: the getter must return exactly the CSR we created the problem with --- */ + status = check_ok(cuOptGetConstraintMatrixCSR(problem, csr_off, csr_col, csr_val), "get CSR matrix"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ints_equal(csr_off, row_offsets, num_constraints + 1) && + ints_equal(csr_col, col_indices, nnz) && + doubles_equal(csr_val, matrix_values, nnz), + "constraint_matrix_csr"); + if (status != CUOPT_SUCCESS) goto DONE; /* --- CSC values: must equal the transpose of the known CSR input (row,col,value multiset) --- */ - C_CHECK(cuOptGetConstraintMatrixCSC(problem, csc_off, csc_row, csc_val)); - if (csc_off[0] != 0 || csc_off[num_variables] != nnz) { - printf("CSC offsets inconsistent with nnz after create\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; - } + status = check_ok(cuOptGetConstraintMatrixCSC(problem, csc_off, csc_row, csc_val), "get CSC matrix"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(csc_off[0] == 0 && csc_off[num_variables] == nnz, "csc_offsets"); + if (status != CUOPT_SUCCESS) goto DONE; for (i = 0; i < num_constraints; ++i) { for (k = row_offsets[i]; k < row_offsets[i + 1]; ++k) { exp_tr[k].row = i; @@ -3729,28 +3410,281 @@ cuopt_int_t test_problem_attributes_created(void) } qsort(exp_tr, (size_t)nnz, sizeof(coo_triple_t), compare_coo); qsort(got_tr, (size_t)nnz, sizeof(coo_triple_t), compare_coo); - for (k = 0; k < nnz; ++k) { - if (exp_tr[k].row != got_tr[k].row || exp_tr[k].col != got_tr[k].col || - exp_tr[k].val != got_tr[k].val) { - printf("CSC entry %d does not match constructed CSR transpose\n", (int)k); - status = CUOPT_VALIDATION_ERROR; - goto DONE; + { + int csc_ok = 1; + for (k = 0; k < nnz; ++k) { + if (exp_tr[k].row != got_tr[k].row || exp_tr[k].col != got_tr[k].col || + exp_tr[k].val != got_tr[k].val) { + csc_ok = 0; + break; + } } + status = check_true(csc_ok, "constraint_matrix_csc (transpose of CSR)"); + if (status != CUOPT_SUCCESS) goto DONE; } /* cuOptCreateProblem does not set variable/row names, so the string-array getter has nothing to - return; asking for num_variables names must be rejected (count != stored 0). Name *values* can - only be verified from a parsed file (covered by the file-based tests). */ - if (cuOptGetProblemStringArrayAttribute( - problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, names_probe, num_variables) != - CUOPT_INVALID_ARGUMENT) { - printf("Expected INVALID_ARGUMENT reading names from an unnamed created problem\n"); - status = CUOPT_VALIDATION_ERROR; - goto DONE; + return; asking for num_variables names must be rejected. Name *values* are verified from a + parsed file in test_problem_attributes_names. */ + status = check_true(cuOptGetProblemStringArrayAttribute( + problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, names_probe, num_variables) == + CUOPT_INVALID_ARGUMENT, + "unnamed problem rejects name getter"); + if (status != CUOPT_SUCCESS) goto DONE; + +DONE: + cuOptDestroyProblem(&problem); + return status; +} + +/* + * Ranged rows can only be built with cuOptCreateRangedProblem, so this is the create-based ground + * truth for the constraint lower/upper bound getters (the sense+rhs create path leaves those empty). + * Build a 2x2 ranged LP and read the row bounds back. + * + * 1 <= x0 + x1 <= 10 + * 0 <= x0 <= 5 + */ +cuopt_int_t test_problem_attributes_ranged(void) +{ + cuOptOptimizationProblem problem = NULL; + cuopt_int_t status = CUOPT_SUCCESS; + + const cuopt_int_t num_constraints = 2; + const cuopt_int_t num_variables = 2; + const cuopt_int_t nnz = 3; + 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}; + + cuopt_int_t ival = 0; + cuopt_float_t fbuf[2]; + + status = check_ok(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), + "cuOptCreateRangedProblem"); + if (status != CUOPT_SUCCESS) goto DONE; + + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &ival), + "get num_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == num_constraints, "num_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &ival), + "get num_variables"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == num_variables, "num_variables"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &ival), + "get num_nonzeros"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == nnz, "num_nonzeros"); + if (status != CUOPT_SUCCESS) goto DONE; + + status = check_ok(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS, fbuf, num_constraints), + "get constraint_lower_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(doubles_equal(fbuf, constraint_lower_bounds, num_constraints), + "constraint_lower_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemFloatArrayAttribute( + problem, CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS, fbuf, num_constraints), + "get constraint_upper_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(doubles_equal(fbuf, constraint_upper_bounds, num_constraints), + "constraint_upper_bounds"); + if (status != CUOPT_SUCCESS) goto DONE; + +DONE: + cuOptDestroyProblem(&problem); + return status; +} + +/* + * Quadratic terms are added with cuOptSetQuadraticObjective / cuOptAddQuadraticConstraint, so this is + * the create-based ground truth for the has_quadratic_objective / has_quadratic_constraints flags. + * Start from a linear problem (both flags 0), add a quadratic objective, then a quadratic + * constraint, and confirm each flag flips to 1. + */ +cuopt_int_t test_problem_attributes_quadratic(void) +{ + cuOptOptimizationProblem problem = NULL; + cuopt_int_t status = CUOPT_SUCCESS; + + 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}; + + /* Quadratic objective term 2 * x0^2 (coordinate/triplet form). */ + 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}; + + /* 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}; + + cuopt_int_t ival = 0; + + status = check_ok(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), + "cuOptCreateProblem"); + if (status != CUOPT_SUCCESS) goto DONE; + + /* Purely linear to start: both flags must read 0. */ + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival), + "get has_quadratic_objective"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 0, "has_quadratic_objective before set"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival), + "get has_quadratic_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 0, "has_quadratic_constraints before add"); + if (status != CUOPT_SUCCESS) goto DONE; + + status = check_ok(cuOptSetQuadraticObjective(problem, 1, q_row, q_col, q_val), + "cuOptSetQuadraticObjective"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival), + "get has_quadratic_objective"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 1, "has_quadratic_objective after set"); + if (status != CUOPT_SUCCESS) goto DONE; + + status = check_ok(cuOptAddQuadraticConstraint( + problem, 1, qc_row, qc_col, qc_val, 1, qc_lin_idx, qc_lin_coeff, CUOPT_LESS_THAN, 5.0), + "cuOptAddQuadraticConstraint"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival), + "get has_quadratic_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == 1, "has_quadratic_constraints after add"); + if (status != CUOPT_SUCCESS) goto DONE; + +DONE: + cuOptDestroyProblem(&problem); + return status; +} + +/* + * Variable and row names are the only attributes with no cuOptCreate / cuOptSet setter, so they are + * the sole getters that require cuOptReadProblem. Write a tiny, hand-inspectable MPS with known + * names, read it back, and check the name getters against ground truth. Every other attribute is + * covered by the create-based tests above. + */ +cuopt_int_t test_problem_attributes_names(const char* mps_path) +{ + cuOptOptimizationProblem problem = NULL; + cuopt_int_t status = CUOPT_SUCCESS; + FILE* f = NULL; + + const cuopt_int_t exp_nv = 2; + const cuopt_int_t exp_nc = 2; + const char* exp_var_names[2] = {"X1", "X2"}; + const char* exp_row_names[2] = {"C1", "C2"}; + + const char* nbuf[2]; + cuopt_int_t ival = 0; + cuopt_int_t i = 0; + + f = fopen(mps_path, "w"); + if (f == NULL) { + printf("names: could not open %s for writing\n", mps_path); + return CUOPT_VALIDATION_ERROR; + } + fprintf(f, + "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"); + fclose(f); + + status = check_ok(cuOptReadProblem(mps_path, &problem), "cuOptReadProblem"); + if (status != CUOPT_SUCCESS) goto DONE; + + /* Dimensions only to size the name buffers; their values are ground-truthed by other tests. */ + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &ival), + "get num_variables"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == exp_nv, "num_variables"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &ival), + "get num_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + status = check_true(ival == exp_nc, "num_constraints"); + if (status != CUOPT_SUCCESS) goto DONE; + + status = check_ok( + cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, nbuf, exp_nv), + "get variable_names"); + if (status != CUOPT_SUCCESS) goto DONE; + for (i = 0; i < exp_nv; ++i) { + status = check_true(nbuf[i] != NULL && strcmp(nbuf[i], exp_var_names[i]) == 0, "variable_names"); + if (status != CUOPT_SUCCESS) goto DONE; + } + status = check_ok( + cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_ROW_NAMES, nbuf, exp_nc), + "get row_names"); + if (status != CUOPT_SUCCESS) goto DONE; + for (i = 0; i < exp_nc; ++i) { + status = check_true(nbuf[i] != NULL && strcmp(nbuf[i], exp_row_names[i]) == 0, "row_names"); + if (status != CUOPT_SUCCESS) goto DONE; } DONE: cuOptDestroyProblem(&problem); + remove(mps_path); return status; -#undef C_CHECK } 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 faf825a832..1ab6a2febe 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 @@ -14,17 +14,10 @@ #include #include -#include -#include -#include #include #include -#include -#include -#include - #include #include @@ -787,206 +780,29 @@ TEST(c_api, gpu_problem_rejects_remote_after_create) EXPECT_EQ(test_gpu_problem_remote_after_create(lp_file.c_str()), CUOPT_SUCCESS); } -namespace { - -/* - * Cross-validate the C API attribute getters against an independent source of truth: parse the same - * file directly with the C++ MPS/QPS parser (mps_data_model_t) and compare. This value-checks the - * getters that cuOptCreateProblem cannot exercise -- variable/row names, objective scaling factor, - * and the quadratic-presence flags -- in addition to the numeric arrays and the CSC matrix. - */ -void verify_attributes_against_parser(const std::string& path) -{ - namespace mo = cuopt::mathematical_optimization; - - const mo::io::mps_data_model_t model = mo::io::read(path); - - cuOptOptimizationProblem problem = nullptr; - ASSERT_EQ(cuOptReadProblem(path.c_str(), &problem), CUOPT_SUCCESS); - - const int nv = model.get_n_variables(); - const int nc = model.get_n_constraints(); - const int nnz = static_cast(model.get_constraint_matrix_values().size()); - - int v = 0; - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &v), CUOPT_SUCCESS); - EXPECT_EQ(v, nv); - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &v), CUOPT_SUCCESS); - EXPECT_EQ(v, nc); - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &v), CUOPT_SUCCESS); - EXPECT_EQ(v, nnz); - - /* num_integers / is_mip derived from the model's variable types (normalized). */ - int expect_integers = 0; - int expect_discrete = 0; - for (char c : model.get_variable_types()) { - const mo::var_t t = mo::char_to_var_type(c); - if (t == mo::var_t::INTEGER) { ++expect_integers; } - if (t == mo::var_t::INTEGER || t == mo::var_t::SEMI_CONTINUOUS) { ++expect_discrete; } - } - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &v), CUOPT_SUCCESS); - EXPECT_EQ(v, expect_integers); - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &v), CUOPT_SUCCESS); - EXPECT_EQ(v, expect_discrete > 0 ? 1 : 0); - - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_OBJECTIVE_SENSE, &v), CUOPT_SUCCESS); - EXPECT_EQ(v, model.get_sense() ? CUOPT_MAXIMIZE : CUOPT_MINIMIZE); - - int qobj = 0, qcon = 0; - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &qobj), - CUOPT_SUCCESS); - EXPECT_EQ(qobj, model.has_quadratic_objective() ? 1 : 0); - EXPECT_EQ(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &qcon), - CUOPT_SUCCESS); - EXPECT_EQ(qcon, model.has_quadratic_constraints() ? 1 : 0); - - double d = 0.0; - EXPECT_EQ(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &d), CUOPT_SUCCESS); - EXPECT_DOUBLE_EQ(d, model.get_objective_offset()); - EXPECT_EQ(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &d), - CUOPT_SUCCESS); - EXPECT_DOUBLE_EQ(d, model.get_objective_scaling_factor()); - - /* Float arrays: compare element-wise against the parsed model. */ - auto check_float_array = [&](cuopt_int_t attr, const std::vector& expected, int count) { - if (expected.empty()) { return; } - ASSERT_EQ(static_cast(expected.size()), count); - std::vector got(static_cast(count)); - ASSERT_EQ(cuOptGetProblemFloatArrayAttribute(problem, attr, got.data(), count), CUOPT_SUCCESS); - for (int i = 0; i < count; ++i) { - EXPECT_DOUBLE_EQ(got[i], expected[i]); - } - }; - check_float_array( - CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, model.get_objective_coefficients(), nv); - check_float_array(CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, model.get_variable_lower_bounds(), nv); - check_float_array(CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, model.get_variable_upper_bounds(), nv); - check_float_array(CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, model.get_constraint_bounds(), nc); - - /* Char arrays: constraint sense passes through; variable types are normalized to 'C'/'I'/'S'. */ - const std::vector& mrt = model.get_row_types(); - if (!mrt.empty()) { - ASSERT_EQ(static_cast(mrt.size()), nc); - std::vector got(static_cast(nc)); - ASSERT_EQ( - cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, got.data(), nc), - CUOPT_SUCCESS); - for (int i = 0; i < nc; ++i) { - EXPECT_EQ(got[i], mrt[i]); - } - } - const std::vector& mvt = model.get_variable_types(); - if (!mvt.empty()) { - ASSERT_EQ(static_cast(mvt.size()), nv); - std::vector got(static_cast(nv)); - ASSERT_EQ( - cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, got.data(), nv), - CUOPT_SUCCESS); - for (int i = 0; i < nv; ++i) { - EXPECT_EQ(got[i], mo::var_type_to_char(mo::char_to_var_type(mvt[i]))); - } - } - - /* String arrays: the key capability with no dedicated getter -- compare names exactly. */ - const std::vector& vnames = model.get_variable_names(); - if (!vnames.empty()) { - ASSERT_EQ(static_cast(vnames.size()), nv); - std::vector got(static_cast(nv)); - ASSERT_EQ(cuOptGetProblemStringArrayAttribute( - problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, got.data(), nv), - CUOPT_SUCCESS); - for (int i = 0; i < nv; ++i) { - EXPECT_EQ(std::string(got[i]), vnames[i]); - } - } - const std::vector& rnames = model.get_row_names(); - if (!rnames.empty()) { - ASSERT_EQ(static_cast(rnames.size()), nc); - std::vector got(static_cast(nc)); - ASSERT_EQ( - cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_ROW_NAMES, got.data(), nc), - CUOPT_SUCCESS); - for (int i = 0; i < nc; ++i) { - EXPECT_EQ(std::string(got[i]), rnames[i]); - } - } - - /* CSC values: must be the transpose of the model's CSR (compare as (row,col,value) multisets). */ - if (nnz > 0) { - const std::vector& moff = model.get_constraint_matrix_offsets(); - const std::vector& mind = model.get_constraint_matrix_indices(); - const std::vector& mval = model.get_constraint_matrix_values(); - std::vector> expected; - expected.reserve(static_cast(nnz)); - for (int i = 0; i < nc; ++i) { - for (int k = moff[i]; k < moff[i + 1]; ++k) { - expected.emplace_back(i, mind[k], mval[k]); - } - } - std::vector csc_off(static_cast(nv + 1)); - std::vector csc_row(static_cast(nnz)); - std::vector csc_val(static_cast(nnz)); - ASSERT_EQ(cuOptGetConstraintMatrixCSC(problem, csc_off.data(), csc_row.data(), csc_val.data()), - CUOPT_SUCCESS); - std::vector> got; - got.reserve(static_cast(nnz)); - for (int c = 0; c < nv; ++c) { - for (int k = csc_off[c]; k < csc_off[c + 1]; ++k) { - got.emplace_back(csc_row[k], c, csc_val[k]); - } - } - std::sort(expected.begin(), expected.end()); - std::sort(got.begin(), got.end()); - EXPECT_EQ(expected, got); - } - - cuOptDestroyProblem(&problem); -} - -} // namespace - -TEST(c_api, problem_attributes) -{ - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - std::string filename = rapidsDatasetRootDir + "/linear_programming/afiro_original.mps"; - EXPECT_EQ(test_problem_attributes(filename.c_str()), CUOPT_SUCCESS); -} - -TEST(c_api, problem_attributes_vs_parser_lp) -{ - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - verify_attributes_against_parser(rapidsDatasetRootDir + "/linear_programming/afiro_original.mps"); -} - -TEST(c_api, problem_attributes_vs_parser_mip) -{ - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - verify_attributes_against_parser(rapidsDatasetRootDir + "/mip/50v-10.mps"); -} - -TEST(c_api, problem_attributes_vs_parser_qp) +// Attribute getters are value-checked against ground truth built with the cuOptCreate / cuOptSet +// interfaces (no parser, no dataset files). cuOptReadProblem is used only where an attribute cannot +// be set through a create/set routine, which is limited to variable/row names. +TEST(c_api, problem_attributes_created) { - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - verify_attributes_against_parser(rapidsDatasetRootDir + "/quadratic_programming/QP_Test_1.qps"); + EXPECT_EQ(test_problem_attributes_created(), CUOPT_SUCCESS); } -TEST(c_api, problem_attributes_mip) +TEST(c_api, problem_attributes_ranged) { - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - std::string filename = rapidsDatasetRootDir + "/mip/50v-10.mps"; - EXPECT_EQ(test_problem_attributes_mip(filename.c_str()), CUOPT_SUCCESS); + EXPECT_EQ(test_problem_attributes_ranged(), CUOPT_SUCCESS); } -TEST(c_api, problem_attributes_qp) +TEST(c_api, problem_attributes_quadratic) { - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - std::string filename = rapidsDatasetRootDir + "/quadratic_programming/QP_Test_1.qps"; - EXPECT_EQ(test_problem_attributes_qp(filename.c_str()), CUOPT_SUCCESS); + EXPECT_EQ(test_problem_attributes_quadratic(), CUOPT_SUCCESS); } -TEST(c_api, problem_attributes_created) +TEST(c_api, problem_attributes_names) { - EXPECT_EQ(test_problem_attributes_created(), CUOPT_SUCCESS); + const std::string mps_path = + std::filesystem::temp_directory_path().string() + "/cuopt_attr_names.mps"; + EXPECT_EQ(test_problem_attributes_names(mps_path.c_str()), CUOPT_SUCCESS); } // Note: cuopt_cli subprocess tests are in Python (test_cpu_only_execution.py) 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 7b38d95f5f..162afd92ea 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 @@ -87,13 +87,13 @@ cuopt_int_t test_cpu_host_create_problem_api(); /* GPU-backed problem created before remote env is set must reject remote solve */ cuopt_int_t test_gpu_problem_remote_after_create(const char* filename); -/* Generic problem attribute getters */ -cuopt_int_t test_problem_attributes(const char* filename); -cuopt_int_t test_problem_attributes_mip(const char* filename); -cuopt_int_t test_problem_attributes_qp(const char* filename); -/* Build a known problem with cuOptCreateProblem and verify getters against the constructed values, - * covering attributes that have no dedicated getter to cross-check against. */ -cuopt_int_t test_problem_attributes_created(void); +/* Problem attribute getters, verified against ground-truth values set through the cuOptCreate / + * cuOptSet interfaces. Each builds a known problem and reads every relevant getter back. */ +cuopt_int_t test_problem_attributes_created(void); /* LP/MIP via cuOptCreateProblem */ +cuopt_int_t test_problem_attributes_ranged(void); /* ranged rows via cuOptCreateRangedProblem */ +cuopt_int_t test_problem_attributes_quadratic(void); /* QP/QCQP flags via cuOptSetQuadratic* */ +/* Names are the only attribute no create/set routine can set: read a tiny MPS with known names. */ +cuopt_int_t test_problem_attributes_names(const char* mps_path); #ifdef __cplusplus } From 64f505d938b5fc29f06c291d2e1462c88b56b066 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 21 Jul 2026 12:00:36 -0400 Subject: [PATCH 5/6] docs: use @note Deprecated for cuOptGetConstraintMatrix to fix doc build Signed-off-by: Trevor McKay --- cpp/include/cuopt/mathematical_optimization/cuopt_c.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h index 0161ca35a9..a287ce7af4 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -566,9 +566,9 @@ cuopt_int_t cuOptGetNumNonZeros(cuOptOptimizationProblem problem, cuopt_int_t* n /** @brief Get the linear constraint matrix of an optimization problem in compressed sparse row * format. This is the matrix of the linear constraints only. * - * @deprecated Use cuOptGetConstraintMatrixCSR (identical CSR output) or - * cuOptGetConstraintMatrixCSC. This function forwards to cuOptGetConstraintMatrixCSR and logs a - * deprecation warning at runtime. + * @note **Deprecated:** Use ``cuOptGetConstraintMatrixCSR`` (identical CSR output) or + * ``cuOptGetConstraintMatrixCSC``. This function forwards to ``cuOptGetConstraintMatrixCSR`` and + * logs a deprecation warning at runtime. * * @param[in] problem - The optimization problem. * From 4fcec52d4a56aeb6d6e6000c8ab071143e164243 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 22 Jul 2026 18:51:13 -0400 Subject: [PATCH 6/6] review feedback on C API additions * simplify tests * move tests to C++ * revise comments * use existing transpose functions in constraint matrix CSC impl --- .../cuopt/mathematical_optimization/cuopt_c.h | 10 +- cpp/src/pdlp/cuopt_c.cpp | 52 +- .../c_api_tests/c_api_test.c | 522 ------------------ .../c_api_tests/c_api_tests.cpp | 275 ++++++++- .../c_api_tests/c_api_tests.h | 8 - 5 files changed, 291 insertions(+), 576 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h index a287ce7af4..b135fed72a 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -566,9 +566,7 @@ cuopt_int_t cuOptGetNumNonZeros(cuOptOptimizationProblem problem, cuopt_int_t* n /** @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`` (identical CSR output) or - * ``cuOptGetConstraintMatrixCSC``. This function forwards to ``cuOptGetConstraintMatrixCSR`` and - * logs a deprecation warning at runtime. + * @note **Deprecated:** Use ``cuOptGetConstraintMatrixCSR``. * * @param[in] problem - The optimization problem. * @@ -591,10 +589,8 @@ 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 in compressed sparse row (CSR) format. - * - * This is the canonical CSR getter; it pairs symmetrically with cuOptGetConstraintMatrixCSC. The - * deprecated cuOptGetConstraintMatrix forwards here. +/** @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. * diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 35ddf52591..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 @@ -29,6 +30,8 @@ #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; @@ -792,7 +795,7 @@ cuopt_int_t cuOptGetConstraintMatrix(cuOptOptimizationProblem problem, cuopt_int_t* constraint_matrix_column_indices_ptr, cuopt_float_t* constraint_matrix_coefficients_ptr) { - CUOPT_LOG_WARN("%s", k_deprecated_get_constraint_matrix_msg); + CUOPT_LOG_ONCE(WARN, "%s", k_deprecated_get_constraint_matrix_msg); return cuOptGetConstraintMatrixCSR(problem, constraint_matrix_row_offsets_ptr, constraint_matrix_column_indices_ptr, @@ -1360,7 +1363,7 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ } /* -------------------------------------------------------------------------- */ -/* Generic problem attribute getters (declared in cuopt_c.h) */ +/* Generic problem attribute getters */ /* -------------------------------------------------------------------------- */ cuopt_int_t cuOptGetProblemIntAttribute(cuOptOptimizationProblem problem, @@ -1541,43 +1544,28 @@ cuopt_int_t cuOptGetConstraintMatrixCSC(cuOptOptimizationProblem problem, const cuopt_int_t n = iface->get_n_variables(); const cuopt_int_t m = iface->get_n_constraints(); - const std::vector row_offsets = iface->get_constraint_matrix_offsets_host(); - const std::vector col_indices = iface->get_constraint_matrix_indices_host(); - const std::vector values = iface->get_constraint_matrix_values_host(); - const cuopt_int_t nnz = static_cast(values.size()); + 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) { - for (cuopt_int_t c = 0; c <= n; ++c) { - column_offsets_ptr[c] = 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; } - // Count non-zeros per column, then prefix-sum into column offsets. - std::vector col_counts(n, 0); - for (cuopt_int_t k = 0; k < nnz; ++k) { - const cuopt_int_t c = col_indices[k]; - if (c < 0 || c >= n) { return CUOPT_VALIDATION_ERROR; } - ++col_counts[c]; - } - column_offsets_ptr[0] = 0; - for (cuopt_int_t c = 0; c < n; ++c) { - column_offsets_ptr[c + 1] = column_offsets_ptr[c] + col_counts[c]; - } + 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); - // Scatter each CSR entry into its CSC position using a running write cursor per column. - std::vector next(column_offsets_ptr, column_offsets_ptr + n); - for (cuopt_int_t i = 0; i < m; ++i) { - const cuopt_int_t row_begin = row_offsets[i]; - const cuopt_int_t row_end = row_offsets[i + 1]; - for (cuopt_int_t k = row_begin; k < row_end; ++k) { - const cuopt_int_t c = col_indices[k]; - const cuopt_int_t dest = next[c]++; - row_indices_ptr[dest] = i; - values_ptr[dest] = values[k]; - } - } + 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_test.c b/cpp/tests/linear_programming/c_api_tests/c_api_test.c index 194637a0d4..cc8d9c842c 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 @@ -3166,525 +3166,3 @@ cuopt_int_t test_gpu_problem_remote_after_create(const char* filename) return status; } - -static int doubles_equal(const cuopt_float_t* a, const cuopt_float_t* b, cuopt_int_t n) -{ - cuopt_int_t i; - for (i = 0; i < n; ++i) { - if (a[i] != b[i]) { return 0; } - } - return 1; -} - -static int ints_equal(const cuopt_int_t* a, const cuopt_int_t* b, cuopt_int_t n) -{ - cuopt_int_t i; - for (i = 0; i < n; ++i) { - if (a[i] != b[i]) { return 0; } - } - return 1; -} - -/* Report a failing C API call. Returns the status so callers can do: - * status = check_ok(cuOptSomeCall(...), "what"); if (status != CUOPT_SUCCESS) goto DONE; */ -static cuopt_int_t check_ok(cuopt_int_t status, const char* what) -{ - if (status != CUOPT_SUCCESS) { printf("FAILED (%d): %s\n", (int)status, what); } - return status; -} - -/* Report a failing ground-truth comparison. Returns CUOPT_SUCCESS when cond holds, otherwise - * CUOPT_VALIDATION_ERROR, so callers can do: - * status = check_true(got == expected, "what"); if (status != CUOPT_SUCCESS) goto DONE; */ -static cuopt_int_t check_true(int cond, const char* what) -{ - if (!cond) { printf("ground-truth failure: %s\n", what); } - return cond ? CUOPT_SUCCESS : CUOPT_VALIDATION_ERROR; -} - -/* One sparse matrix entry, used to compare CSR and CSC as an unordered (row,col,value) multiset. */ -typedef struct { - cuopt_int_t row; - cuopt_int_t col; - cuopt_float_t val; -} coo_triple_t; - -/* Total order by (row, col, value) so two equivalent matrices sort to identical sequences. */ -static int compare_coo(const void* a, const void* b) -{ - const coo_triple_t* ta = (const coo_triple_t*)a; - const coo_triple_t* tb = (const coo_triple_t*)b; - if (ta->row != tb->row) { return ta->row < tb->row ? -1 : 1; } - if (ta->col != tb->col) { return ta->col < tb->col ? -1 : 1; } - if (ta->val != tb->val) { return ta->val < tb->val ? -1 : 1; } - return 0; -} - -/* - * Build a small mixed-integer LP by hand with cuOptCreateProblem, then read every getter back and - * compare against the exact values we constructed. Ground truth comes from the create call itself, - * so there is no dependence on a parser or a dataset file, and it works on either backend. This - * covers every attribute cuOptCreateProblem can set; the ranged-only, quadratic-only, and name-only - * attributes are covered by test_problem_attributes_ranged, _quadratic, and _names respectively. - */ -cuopt_int_t test_problem_attributes_created(void) -{ - cuOptOptimizationProblem problem = NULL; - cuopt_int_t status = CUOPT_SUCCESS; - - /* Known construction: 2 constraints x 3 variables, mixed integer. - * A = [ 1 0 2 ] sense=L rhs=10 - * [ 0 3 4 ] sense=G rhs=20 - * obj = 5 + [1,2,3].x var types = [I,C,I] */ - 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}; - - cuopt_float_t fbuf[3]; - char cbuf[3]; - cuopt_int_t ival = 0; - cuopt_float_t fval = 0.0; - cuopt_int_t csr_off[3]; - cuopt_int_t csr_col[4]; - cuopt_float_t csr_val[4]; - cuopt_int_t csc_off[4]; - cuopt_int_t csc_row[4]; - cuopt_float_t csc_val[4]; - coo_triple_t exp_tr[4]; - coo_triple_t got_tr[4]; - cuopt_int_t i = 0, k = 0; - const char* names_probe[3]; - - status = check_ok(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), - "cuOptCreateProblem"); - if (status != CUOPT_SUCCESS) goto DONE; - - /* --- dimensions and objective sense: generic getters vs the construction --- */ - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &ival), - "get num_variables"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == num_variables, "num_variables"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &ival), - "get num_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == num_constraints, "num_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &ival), - "get num_nonzeros"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == nnz, "num_nonzeros"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_OBJECTIVE_SENSE, &ival), - "get objective_sense"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == CUOPT_MINIMIZE, "objective_sense"); - if (status != CUOPT_SUCCESS) goto DONE; - - /* --- scalar attributes with no dedicated getter: verify against the known construction --- */ - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_INTEGERS, &ival), - "get num_integers"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 2, "num_integers"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_PROBLEM_CATEGORY, &ival), - "get problem_category"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 1 /* MIP */, "problem_category"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_IS_MIP, &ival), "get is_mip"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 1, "is_mip"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival), - "get has_quadratic_objective"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 0, "has_quadratic_objective"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival), - "get has_quadratic_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 0, "has_quadratic_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_SCALING_FACTOR, &fval), - "get objective_scaling_factor"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(fval == 1.0, "objective_scaling_factor"); /* create leaves the default of 1 */ - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemFloatAttribute(problem, CUOPT_ATTR_OBJECTIVE_OFFSET, &fval), - "get objective_offset"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(fval == objective_offset, "objective_offset"); - if (status != CUOPT_SUCCESS) goto DONE; - - /* --- array getters: compare against the exact values we constructed with --- */ - status = check_ok(cuOptGetProblemFloatArrayAttribute( - problem, CUOPT_ARRAY_ATTR_OBJECTIVE_COEFFICIENTS, fbuf, num_variables), - "get objective_coefficients"); - if (status != CUOPT_SUCCESS) goto DONE; - status = - check_true(doubles_equal(fbuf, objective_coefficients, num_variables), "objective_coefficients"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemFloatArrayAttribute( - problem, CUOPT_ARRAY_ATTR_VARIABLE_LOWER_BOUNDS, fbuf, num_variables), - "get variable_lower_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(doubles_equal(fbuf, lower_bounds, num_variables), "variable_lower_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemFloatArrayAttribute( - problem, CUOPT_ARRAY_ATTR_VARIABLE_UPPER_BOUNDS, fbuf, num_variables), - "get variable_upper_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(doubles_equal(fbuf, upper_bounds, num_variables), "variable_upper_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok( - cuOptGetProblemFloatArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_RHS, fbuf, num_constraints), - "get constraint_rhs"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(doubles_equal(fbuf, rhs, num_constraints), "constraint_rhs"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok( - cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_CONSTRAINT_SENSE, cbuf, num_constraints), - "get constraint_sense"); - if (status != CUOPT_SUCCESS) goto DONE; - status = - check_true(memcmp(cbuf, constraint_sense, (size_t)num_constraints) == 0, "constraint_sense"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok( - cuOptGetProblemCharArrayAttribute(problem, CUOPT_ARRAY_ATTR_VARIABLE_TYPES, cbuf, num_variables), - "get variable_types"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(memcmp(cbuf, variable_types, (size_t)num_variables) == 0, "variable_types"); - if (status != CUOPT_SUCCESS) goto DONE; - - /* --- CSR values: the getter must return exactly the CSR we created the problem with --- */ - status = check_ok(cuOptGetConstraintMatrixCSR(problem, csr_off, csr_col, csr_val), "get CSR matrix"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ints_equal(csr_off, row_offsets, num_constraints + 1) && - ints_equal(csr_col, col_indices, nnz) && - doubles_equal(csr_val, matrix_values, nnz), - "constraint_matrix_csr"); - if (status != CUOPT_SUCCESS) goto DONE; - - /* --- CSC values: must equal the transpose of the known CSR input (row,col,value multiset) --- */ - status = check_ok(cuOptGetConstraintMatrixCSC(problem, csc_off, csc_row, csc_val), "get CSC matrix"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(csc_off[0] == 0 && csc_off[num_variables] == nnz, "csc_offsets"); - if (status != CUOPT_SUCCESS) goto DONE; - for (i = 0; i < num_constraints; ++i) { - for (k = row_offsets[i]; k < row_offsets[i + 1]; ++k) { - exp_tr[k].row = i; - exp_tr[k].col = col_indices[k]; - exp_tr[k].val = matrix_values[k]; - } - } - for (i = 0; i < num_variables; ++i) { - for (k = csc_off[i]; k < csc_off[i + 1]; ++k) { - got_tr[k].row = csc_row[k]; - got_tr[k].col = i; - got_tr[k].val = csc_val[k]; - } - } - qsort(exp_tr, (size_t)nnz, sizeof(coo_triple_t), compare_coo); - qsort(got_tr, (size_t)nnz, sizeof(coo_triple_t), compare_coo); - { - int csc_ok = 1; - for (k = 0; k < nnz; ++k) { - if (exp_tr[k].row != got_tr[k].row || exp_tr[k].col != got_tr[k].col || - exp_tr[k].val != got_tr[k].val) { - csc_ok = 0; - break; - } - } - status = check_true(csc_ok, "constraint_matrix_csc (transpose of CSR)"); - if (status != CUOPT_SUCCESS) goto DONE; - } - - /* cuOptCreateProblem does not set variable/row names, so the string-array getter has nothing to - return; asking for num_variables names must be rejected. Name *values* are verified from a - parsed file in test_problem_attributes_names. */ - status = check_true(cuOptGetProblemStringArrayAttribute( - problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, names_probe, num_variables) == - CUOPT_INVALID_ARGUMENT, - "unnamed problem rejects name getter"); - if (status != CUOPT_SUCCESS) goto DONE; - -DONE: - cuOptDestroyProblem(&problem); - return status; -} - -/* - * Ranged rows can only be built with cuOptCreateRangedProblem, so this is the create-based ground - * truth for the constraint lower/upper bound getters (the sense+rhs create path leaves those empty). - * Build a 2x2 ranged LP and read the row bounds back. - * - * 1 <= x0 + x1 <= 10 - * 0 <= x0 <= 5 - */ -cuopt_int_t test_problem_attributes_ranged(void) -{ - cuOptOptimizationProblem problem = NULL; - cuopt_int_t status = CUOPT_SUCCESS; - - const cuopt_int_t num_constraints = 2; - const cuopt_int_t num_variables = 2; - const cuopt_int_t nnz = 3; - 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}; - - cuopt_int_t ival = 0; - cuopt_float_t fbuf[2]; - - status = check_ok(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), - "cuOptCreateRangedProblem"); - if (status != CUOPT_SUCCESS) goto DONE; - - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &ival), - "get num_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == num_constraints, "num_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &ival), - "get num_variables"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == num_variables, "num_variables"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_NONZEROS, &ival), - "get num_nonzeros"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == nnz, "num_nonzeros"); - if (status != CUOPT_SUCCESS) goto DONE; - - status = check_ok(cuOptGetProblemFloatArrayAttribute( - problem, CUOPT_ARRAY_ATTR_CONSTRAINT_LOWER_BOUNDS, fbuf, num_constraints), - "get constraint_lower_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(doubles_equal(fbuf, constraint_lower_bounds, num_constraints), - "constraint_lower_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemFloatArrayAttribute( - problem, CUOPT_ARRAY_ATTR_CONSTRAINT_UPPER_BOUNDS, fbuf, num_constraints), - "get constraint_upper_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(doubles_equal(fbuf, constraint_upper_bounds, num_constraints), - "constraint_upper_bounds"); - if (status != CUOPT_SUCCESS) goto DONE; - -DONE: - cuOptDestroyProblem(&problem); - return status; -} - -/* - * Quadratic terms are added with cuOptSetQuadraticObjective / cuOptAddQuadraticConstraint, so this is - * the create-based ground truth for the has_quadratic_objective / has_quadratic_constraints flags. - * Start from a linear problem (both flags 0), add a quadratic objective, then a quadratic - * constraint, and confirm each flag flips to 1. - */ -cuopt_int_t test_problem_attributes_quadratic(void) -{ - cuOptOptimizationProblem problem = NULL; - cuopt_int_t status = CUOPT_SUCCESS; - - 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}; - - /* Quadratic objective term 2 * x0^2 (coordinate/triplet form). */ - 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}; - - /* 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}; - - cuopt_int_t ival = 0; - - status = check_ok(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), - "cuOptCreateProblem"); - if (status != CUOPT_SUCCESS) goto DONE; - - /* Purely linear to start: both flags must read 0. */ - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival), - "get has_quadratic_objective"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 0, "has_quadratic_objective before set"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival), - "get has_quadratic_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 0, "has_quadratic_constraints before add"); - if (status != CUOPT_SUCCESS) goto DONE; - - status = check_ok(cuOptSetQuadraticObjective(problem, 1, q_row, q_col, q_val), - "cuOptSetQuadraticObjective"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_OBJECTIVE, &ival), - "get has_quadratic_objective"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 1, "has_quadratic_objective after set"); - if (status != CUOPT_SUCCESS) goto DONE; - - status = check_ok(cuOptAddQuadraticConstraint( - problem, 1, qc_row, qc_col, qc_val, 1, qc_lin_idx, qc_lin_coeff, CUOPT_LESS_THAN, 5.0), - "cuOptAddQuadraticConstraint"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_HAS_QUADRATIC_CONSTRAINTS, &ival), - "get has_quadratic_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == 1, "has_quadratic_constraints after add"); - if (status != CUOPT_SUCCESS) goto DONE; - -DONE: - cuOptDestroyProblem(&problem); - return status; -} - -/* - * Variable and row names are the only attributes with no cuOptCreate / cuOptSet setter, so they are - * the sole getters that require cuOptReadProblem. Write a tiny, hand-inspectable MPS with known - * names, read it back, and check the name getters against ground truth. Every other attribute is - * covered by the create-based tests above. - */ -cuopt_int_t test_problem_attributes_names(const char* mps_path) -{ - cuOptOptimizationProblem problem = NULL; - cuopt_int_t status = CUOPT_SUCCESS; - FILE* f = NULL; - - const cuopt_int_t exp_nv = 2; - const cuopt_int_t exp_nc = 2; - const char* exp_var_names[2] = {"X1", "X2"}; - const char* exp_row_names[2] = {"C1", "C2"}; - - const char* nbuf[2]; - cuopt_int_t ival = 0; - cuopt_int_t i = 0; - - f = fopen(mps_path, "w"); - if (f == NULL) { - printf("names: could not open %s for writing\n", mps_path); - return CUOPT_VALIDATION_ERROR; - } - fprintf(f, - "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"); - fclose(f); - - status = check_ok(cuOptReadProblem(mps_path, &problem), "cuOptReadProblem"); - if (status != CUOPT_SUCCESS) goto DONE; - - /* Dimensions only to size the name buffers; their values are ground-truthed by other tests. */ - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_VARIABLES, &ival), - "get num_variables"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == exp_nv, "num_variables"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_ok(cuOptGetProblemIntAttribute(problem, CUOPT_ATTR_NUM_CONSTRAINTS, &ival), - "get num_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - status = check_true(ival == exp_nc, "num_constraints"); - if (status != CUOPT_SUCCESS) goto DONE; - - status = check_ok( - cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_VARIABLE_NAMES, nbuf, exp_nv), - "get variable_names"); - if (status != CUOPT_SUCCESS) goto DONE; - for (i = 0; i < exp_nv; ++i) { - status = check_true(nbuf[i] != NULL && strcmp(nbuf[i], exp_var_names[i]) == 0, "variable_names"); - if (status != CUOPT_SUCCESS) goto DONE; - } - status = check_ok( - cuOptGetProblemStringArrayAttribute(problem, CUOPT_STRING_ARRAY_ROW_NAMES, nbuf, exp_nc), - "get row_names"); - if (status != CUOPT_SUCCESS) goto DONE; - for (i = 0; i < exp_nc; ++i) { - status = check_true(nbuf[i] != NULL && strcmp(nbuf[i], exp_row_names[i]) == 0, "row_names"); - if (status != CUOPT_SUCCESS) goto DONE; - } - -DONE: - cuOptDestroyProblem(&problem); - remove(mps_path); - 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 1ab6a2febe..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,29 +784,286 @@ 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 value-checked against ground truth built with the cuOptCreate / cuOptSet -// interfaces (no parser, no dataset files). cuOptReadProblem is used only where an attribute cannot -// be set through a create/set routine, which is limited to variable/row names. +// 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) { - EXPECT_EQ(test_problem_attributes_created(), CUOPT_SUCCESS); + // 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) { - EXPECT_EQ(test_problem_attributes_ranged(), CUOPT_SUCCESS); + 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) { - EXPECT_EQ(test_problem_attributes_quadratic(), CUOPT_SUCCESS); + 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"; - EXPECT_EQ(test_problem_attributes_names(mps_path.c_str()), CUOPT_SUCCESS); + { + 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) 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 162afd92ea..a8c3a1f4e4 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 @@ -87,14 +87,6 @@ cuopt_int_t test_cpu_host_create_problem_api(); /* GPU-backed problem created before remote env is set must reject remote solve */ cuopt_int_t test_gpu_problem_remote_after_create(const char* filename); -/* Problem attribute getters, verified against ground-truth values set through the cuOptCreate / - * cuOptSet interfaces. Each builds a known problem and reads every relevant getter back. */ -cuopt_int_t test_problem_attributes_created(void); /* LP/MIP via cuOptCreateProblem */ -cuopt_int_t test_problem_attributes_ranged(void); /* ranged rows via cuOptCreateRangedProblem */ -cuopt_int_t test_problem_attributes_quadratic(void); /* QP/QCQP flags via cuOptSetQuadratic* */ -/* Names are the only attribute no create/set routine can set: read a tiny MPS with known names. */ -cuopt_int_t test_problem_attributes_names(const char* mps_path); - #ifdef __cplusplus } #endif