From 8a4ee93c480cddab26d491ce91d033567405484a Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 7 Jul 2026 10:27:27 -0400 Subject: [PATCH 1/2] 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 5e720a645e..d441c9e864 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -214,4 +214,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 06baf7a0217930589fbbf5b17cebfd59af3f8a8d Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 10 Jul 2026 14:40:59 -0400 Subject: [PATCH 2/2] 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 d441c9e864..24a6c5a000 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -214,7 +214,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 }