From da20f08796d50768c44ce0013e51ed557d2df682 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 12 Aug 2026 17:26:52 -0500 Subject: [PATCH 1/3] Expose solver statistics as scalar solution attributes The C API reports the outcome of a solve - status, objective, primal and dual solution, MIP gap, solution bound, solve time - but none of the diagnostics the C++ solution interfaces already carry. Of the eleven fields tracked in #1202 (primal and dual residual, gap, iteration count, solved-by, presolve time, node count, simplex iterations, and the three violation magnitudes), zero are reachable today. Python sees all of them by binding to the C++ structs through Cython, so the C ABI is a second-class path and every non-Python binding hits the same wall. Add cuOptGetSolutionIntAttribute and cuOptGetSolutionFloatAttribute, reading the CUOPT_SOLUTION_ATTR_* selectors in constants.h. Attributes rather than one getter per statistic, for three reasons: it matches how problem data is already read; a future statistic becomes a new constant instead of a new exported symbol, so existing callers need no relink; and bindings that generate from constants.h pick new statistics up with no hand-written code. Selectors live in their own numeric range so a problem selector passed to a solution accessor, or the reverse, is rejected rather than silently read. LP selectors require an LP solution and MIP selectors require a MIP solution, since the two come from different solvers; CUOPT_ATTR_IS_MIP on the originating problem says which set applies. The data is read straight off lp_solution_interface_t and mip_solution_interface_t, so this is exposure only - no computation and no solve-time cost. Tests cover both solvers and the ways a caller can get it wrong: a float selector through the integer accessor and the reverse, the other solver's selectors, unknown selectors, and null arguments. Float outputs are seeded with NaN rather than a numeric sentinel, so an accessor that never writes is caught rather than mistaken for a real result. Split out of #1524 so the C API can be reviewed on its own. Signed-off-by: Ramakrishna Prabhu --- .../mathematical_optimization/constants.h | 21 +++ .../cuopt/mathematical_optimization/cuopt_c.h | 48 ++++++ cpp/src/pdlp/cuopt_c.cpp | 111 ++++++++++++++ .../c_api_tests/c_api_tests.cpp | 138 ++++++++++++++++++ 4 files changed, 318 insertions(+) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 04ac3355ab..28d9bd3733 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -272,4 +272,25 @@ #define CUOPT_STRING_ARRAY_VARIABLE_NAMES 200 #define CUOPT_STRING_ARRAY_ROW_NAMES 201 +/* @brief Scalar solution attribute selectors + * (see cuOptGetSolution{Int,Float}Attribute). Passed as cuopt_int_t; numbered in a separate + * range from the problem selectors so a problem selector passed to a solution accessor, or the + * reverse, is rejected rather than silently read. + * + * LP attributes require an LP solution and MIP attributes require a MIP solution; the accessors + * return CUOPT_INVALID_ARGUMENT otherwise. Use CUOPT_ATTR_IS_MIP on the originating problem to + * tell which set applies. + */ +#define CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL 300 +#define CUOPT_SOLUTION_ATTR_LP_DUAL_RESIDUAL 301 +#define CUOPT_SOLUTION_ATTR_LP_GAP 302 +#define CUOPT_SOLUTION_ATTR_LP_NUM_ITERATIONS 303 +#define CUOPT_SOLUTION_ATTR_LP_SOLVED_BY 304 +#define CUOPT_SOLUTION_ATTR_MIP_PRESOLVE_TIME 305 +#define CUOPT_SOLUTION_ATTR_MIP_NUM_NODES 306 +#define CUOPT_SOLUTION_ATTR_MIP_NUM_SIMPLEX_ITERATIONS 307 +#define CUOPT_SOLUTION_ATTR_MIP_MAX_CONSTRAINT_VIOLATION 308 +#define CUOPT_SOLUTION_ATTR_MIP_MAX_INT_VIOLATION 309 +#define CUOPT_SOLUTION_ATTR_MIP_MAX_VARIABLE_BOUND_VIOLATION 310 + #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 82f0fd281f..15f4aa4f51 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -1110,6 +1110,54 @@ cuopt_int_t cuOptGetDualObjectiveValue(cuOptSolution solution, */ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_cost_ptr); +/* -------------------------------------------------------------------------- */ +/* Solution attributes */ +/* -------------------------------------------------------------------------- */ + +/* + * Solver statistics are read as scalar attributes, using the CUOPT_SOLUTION_ATTR_* selectors in + * constants.h. New statistics can then be added as constants rather than as new exported + * functions, which keeps the ABI stable for existing callers. + * + * LP and MIP statistics come from different solvers, so an LP selector requires an LP solution + * and a MIP selector requires a MIP solution. Query CUOPT_ATTR_IS_MIP on the originating problem + * to decide which set applies. + */ + +/** @brief Get a scalar integer solution attribute (a CUOPT_SOLUTION_ATTR_* with an integer + * value: iteration counts, node counts, or the method that solved the problem). + * + * @param[in] solution - The solution object. + * + * @param[in] attribute - The attribute selector. + * + * @param[out] value_out - A pointer to a cuopt_int_t that on output will contain the value. + * + * @return A status code indicating success or failure. Returns CUOPT_INVALID_ARGUMENT if the + * selector is unknown, does not have an integer value, or does not apply to this solution's + * solver. + */ +cuopt_int_t cuOptGetSolutionIntAttribute(cuOptSolution solution, + cuopt_int_t attribute, + cuopt_int_t* value_out); + +/** @brief Get a scalar floating-point solution attribute (a CUOPT_SOLUTION_ATTR_* with a + * floating-point value: residuals, gap, presolve time, or violation magnitudes). + * + * @param[in] solution - The solution object. + * + * @param[in] attribute - The attribute selector. + * + * @param[out] value_out - A pointer to a cuopt_float_t that on output will contain the value. + * + * @return A status code indicating success or failure. Returns CUOPT_INVALID_ARGUMENT if the + * selector is unknown, does not have a floating-point value, or does not apply to this + * solution's solver. + */ +cuopt_int_t cuOptGetSolutionFloatAttribute(cuOptSolution solution, + cuopt_int_t attribute, + cuopt_float_t* value_out); + /* -------------------------------------------------------------------------- */ /* Generic problem attributes */ /* -------------------------------------------------------------------------- */ diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index a813abf71f..3614c8d482 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -34,6 +35,8 @@ 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::lp_solution_interface_t; +using cuopt::mathematical_optimization::mip_solution_interface_t; using cuopt::mathematical_optimization::optimization_problem_interface_t; using cuopt::mathematical_optimization::problem_and_stream_view_t; using cuopt::mathematical_optimization::problem_category_t; @@ -1362,6 +1365,114 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ } } +namespace { + +// Solution attribute plumbing. Each selector names one scalar on the LP or MIP solution +// interface; adding a statistic later means adding a constant and one case, not a new symbol. + +bool is_lp_solution_attribute(cuopt_int_t attribute) +{ + return attribute >= CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL && + attribute <= CUOPT_SOLUTION_ATTR_LP_SOLVED_BY; +} + +lp_solution_interface_t* as_lp_solution(cuOptSolution solution) +{ + auto* view = static_cast(solution); + return view->is_mip ? nullptr : view->lp_solution_interface_ptr; +} + +mip_solution_interface_t* as_mip_solution(cuOptSolution solution) +{ + auto* view = static_cast(solution); + return view->is_mip ? view->mip_solution_interface_ptr : nullptr; +} + +} // namespace + +cuopt_int_t cuOptGetSolutionIntAttribute(cuOptSolution solution, + cuopt_int_t attribute, + cuopt_int_t* value_out) +{ + if (solution == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (value_out == nullptr) { return CUOPT_INVALID_ARGUMENT; } + + try { + if (is_lp_solution_attribute(attribute)) { + auto* lp = as_lp_solution(solution); + if (lp == nullptr) { return CUOPT_INVALID_ARGUMENT; } + switch (attribute) { + case CUOPT_SOLUTION_ATTR_LP_NUM_ITERATIONS: + *value_out = static_cast(lp->get_num_iterations()); + return CUOPT_SUCCESS; + case CUOPT_SOLUTION_ATTR_LP_SOLVED_BY: + *value_out = static_cast(lp->solved_by()); + return CUOPT_SUCCESS; + default: return CUOPT_INVALID_ARGUMENT; // a float-valued LP selector + } + } + + auto* mip = as_mip_solution(solution); + if (mip == nullptr) { return CUOPT_INVALID_ARGUMENT; } + switch (attribute) { + case CUOPT_SOLUTION_ATTR_MIP_NUM_NODES: + *value_out = static_cast(mip->get_num_nodes()); + return CUOPT_SUCCESS; + case CUOPT_SOLUTION_ATTR_MIP_NUM_SIMPLEX_ITERATIONS: + *value_out = static_cast(mip->get_num_simplex_iterations()); + return CUOPT_SUCCESS; + default: return CUOPT_INVALID_ARGUMENT; + } + } catch (const std::exception& e) { + return CUOPT_RUNTIME_ERROR; + } +} + +cuopt_int_t cuOptGetSolutionFloatAttribute(cuOptSolution solution, + cuopt_int_t attribute, + cuopt_float_t* value_out) +{ + if (solution == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (value_out == nullptr) { return CUOPT_INVALID_ARGUMENT; } + + try { + if (is_lp_solution_attribute(attribute)) { + auto* lp = as_lp_solution(solution); + if (lp == nullptr) { return CUOPT_INVALID_ARGUMENT; } + switch (attribute) { + case CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL: + *value_out = lp->get_l2_primal_residual(); + return CUOPT_SUCCESS; + case CUOPT_SOLUTION_ATTR_LP_DUAL_RESIDUAL: + *value_out = lp->get_l2_dual_residual(); + return CUOPT_SUCCESS; + case CUOPT_SOLUTION_ATTR_LP_GAP: *value_out = lp->get_gap(); return CUOPT_SUCCESS; + default: return CUOPT_INVALID_ARGUMENT; // an int-valued LP selector + } + } + + auto* mip = as_mip_solution(solution); + if (mip == nullptr) { return CUOPT_INVALID_ARGUMENT; } + switch (attribute) { + case CUOPT_SOLUTION_ATTR_MIP_PRESOLVE_TIME: + *value_out = mip->get_presolve_time(); + return CUOPT_SUCCESS; + case CUOPT_SOLUTION_ATTR_MIP_MAX_CONSTRAINT_VIOLATION: + *value_out = mip->get_max_constraint_violation(); + return CUOPT_SUCCESS; + case CUOPT_SOLUTION_ATTR_MIP_MAX_INT_VIOLATION: + *value_out = mip->get_max_int_violation(); + return CUOPT_SUCCESS; + case CUOPT_SOLUTION_ATTR_MIP_MAX_VARIABLE_BOUND_VIOLATION: + *value_out = mip->get_max_variable_bound_violation(); + return CUOPT_SUCCESS; + default: return CUOPT_INVALID_ARGUMENT; + } + } catch (const std::exception& e) { + return CUOPT_RUNTIME_ERROR; + } +} + /* -------------------------------------------------------------------------- */ /* Generic problem attribute getters */ /* -------------------------------------------------------------------------- */ 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 ed0e017cae..12cf870e67 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp @@ -7,7 +7,9 @@ #include "c_api_tests.h" +#include #include +#include #include #include #include @@ -1077,3 +1079,139 @@ TEST(c_api, problem_attributes_names) // Note: cuopt_cli subprocess tests are in Python (test_cpu_only_execution.py) // which provides better cross-platform subprocess handling + +// ============================================================================= +// Solution attributes +// +// Solver statistics are read through the scalar solution attribute accessors rather than +// dedicated getters, so a new statistic is a new constant instead of a new exported symbol. +// ============================================================================= + +namespace { + +// Builds and solves a two-variable problem, integral when `mip` is set. +cuOptSolution solve_tiny_problem(bool mip) +{ + cuopt_int_t row_offsets[] = {0, 2}; + cuopt_int_t column_indices[] = {0, 1}; + cuopt_float_t matrix_values[] = {1.0, 1.0}; + cuopt_float_t objective[] = {-1.0, -1.0}; + cuopt_float_t rhs[] = {3.5}; + char constraint_sense[] = {CUOPT_LESS_THAN}; + cuopt_float_t lower_bounds[] = {0.0, 0.0}; + cuopt_float_t upper_bounds[] = {10.0, 10.0}; + char variable_types[] = {mip ? CUOPT_INTEGER : CUOPT_CONTINUOUS, + mip ? CUOPT_INTEGER : CUOPT_CONTINUOUS}; + + cuOptOptimizationProblem problem = nullptr; + cuOptSolverSettings settings = nullptr; + cuOptSolution solution = nullptr; + EXPECT_EQ(cuOptCreateProblem(1, + 2, + CUOPT_MINIMIZE, + 0, + objective, + row_offsets, + column_indices, + matrix_values, + constraint_sense, + rhs, + lower_bounds, + upper_bounds, + variable_types, + &problem), + CUOPT_SUCCESS); + EXPECT_EQ(cuOptCreateSolverSettings(&settings), CUOPT_SUCCESS); + EXPECT_EQ(cuOptSolve(problem, settings, &solution), CUOPT_SUCCESS); + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + return solution; +} + +} // namespace + +TEST(c_api, lp_solution_attributes) +{ + cuOptSolution solution = solve_tiny_problem(false); + ASSERT_NE(solution, nullptr); + + // Seed with NaN rather than a numeric sentinel: the solver cannot legitimately report NaN, + // so "still NaN" means the accessor never wrote the value. A numeric sentinel would be + // indistinguishable from a real result. + for (cuopt_int_t attribute : {CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL, + CUOPT_SOLUTION_ATTR_LP_DUAL_RESIDUAL, + CUOPT_SOLUTION_ATTR_LP_GAP}) { + cuopt_float_t value = std::nan(""); + ASSERT_EQ(cuOptGetSolutionFloatAttribute(solution, attribute, &value), CUOPT_SUCCESS) + << "attribute " << attribute; + EXPECT_FALSE(std::isnan(value)) << "attribute " << attribute; + } + cuopt_float_t primal_residual = std::nan(""); + ASSERT_EQ(cuOptGetSolutionFloatAttribute( + solution, CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL, &primal_residual), + CUOPT_SUCCESS); + EXPECT_GE(primal_residual, 0.0); + + for (cuopt_int_t attribute : + {CUOPT_SOLUTION_ATTR_LP_NUM_ITERATIONS, CUOPT_SOLUTION_ATTR_LP_SOLVED_BY}) { + cuopt_int_t value = -1; + ASSERT_EQ(cuOptGetSolutionIntAttribute(solution, attribute, &value), CUOPT_SUCCESS) + << "attribute " << attribute; + EXPECT_GE(value, 0) << "attribute " << attribute; + } + + // Asking for a float attribute through the int accessor, and the reverse, is rejected. + cuopt_int_t as_int = 0; + cuopt_float_t as_float = 0; + EXPECT_EQ(cuOptGetSolutionIntAttribute(solution, CUOPT_SOLUTION_ATTR_LP_GAP, &as_int), + CUOPT_INVALID_ARGUMENT); + EXPECT_EQ( + cuOptGetSolutionFloatAttribute(solution, CUOPT_SOLUTION_ATTR_LP_NUM_ITERATIONS, &as_float), + CUOPT_INVALID_ARGUMENT); + + // MIP selectors do not apply to an LP solution. + EXPECT_EQ(cuOptGetSolutionIntAttribute(solution, CUOPT_SOLUTION_ATTR_MIP_NUM_NODES, &as_int), + CUOPT_INVALID_ARGUMENT); + + // Unknown selectors and null arguments are rejected. + EXPECT_EQ(cuOptGetSolutionIntAttribute(solution, 99999, &as_int), CUOPT_INVALID_ARGUMENT); + EXPECT_EQ(cuOptGetSolutionFloatAttribute(solution, CUOPT_SOLUTION_ATTR_LP_GAP, nullptr), + CUOPT_INVALID_ARGUMENT); + EXPECT_EQ(cuOptGetSolutionFloatAttribute(nullptr, CUOPT_SOLUTION_ATTR_LP_GAP, &as_float), + CUOPT_INVALID_ARGUMENT); + + cuOptDestroySolution(&solution); +} + +TEST(c_api, mip_solution_attributes) +{ + cuOptSolution solution = solve_tiny_problem(true); + ASSERT_NE(solution, nullptr); + + // Violations are magnitudes, so they cannot be negative. + for (cuopt_int_t attribute : {CUOPT_SOLUTION_ATTR_MIP_PRESOLVE_TIME, + CUOPT_SOLUTION_ATTR_MIP_MAX_CONSTRAINT_VIOLATION, + CUOPT_SOLUTION_ATTR_MIP_MAX_INT_VIOLATION, + CUOPT_SOLUTION_ATTR_MIP_MAX_VARIABLE_BOUND_VIOLATION}) { + cuopt_float_t value = std::nan(""); + ASSERT_EQ(cuOptGetSolutionFloatAttribute(solution, attribute, &value), CUOPT_SUCCESS) + << "attribute " << attribute; + EXPECT_FALSE(std::isnan(value)) << "attribute " << attribute; + EXPECT_GE(value, 0.0) << "attribute " << attribute; + } + + for (cuopt_int_t attribute : + {CUOPT_SOLUTION_ATTR_MIP_NUM_NODES, CUOPT_SOLUTION_ATTR_MIP_NUM_SIMPLEX_ITERATIONS}) { + cuopt_int_t value = -1; + ASSERT_EQ(cuOptGetSolutionIntAttribute(solution, attribute, &value), CUOPT_SUCCESS) + << "attribute " << attribute; + EXPECT_GE(value, 0) << "attribute " << attribute; + } + + // LP selectors do not apply to a MIP solution. + cuopt_float_t as_float = 0; + EXPECT_EQ(cuOptGetSolutionFloatAttribute(solution, CUOPT_SOLUTION_ATTR_LP_GAP, &as_float), + CUOPT_INVALID_ARGUMENT); + + cuOptDestroySolution(&solution); +} From e6870345d753a8762afa23ffbeae199a964fb43a Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 13 Aug 2026 15:20:17 -0500 Subject: [PATCH 2/3] Address review on the solution attribute accessors Documentation, per mlubin: - Drop the rationale about adding statistics as constants rather than exported functions. That is an argument for the design, which belongs in the pull request, not in a public header describing what the accessors do. - Stop describing LP and MIP as different solvers. That leaks an implementation detail and is not vocabulary the C API uses anywhere else. Availability is now stated in terms of the class of problem that produced the solution. - Define what a solution attribute is and how it differs from a parameter, which the header introduced without saying: a parameter is an input, set on a cuOptSolverSettings before solving; an attribute is an output, read from a solved solution or from a problem. Test cleanup, per CodeRabbit: the attribute checks use ASSERT, which returns early on failure, so the explicit cuOptDestroySolution at the end of each test was skipped exactly when a test failed, leaking the solution into the rest of the binary. A scoped guard now destroys it on every exit path, which keeps the fail-fast assertions rather than weakening them to EXPECT. Verified: 69/69 C_API_TEST cases pass. Signed-off-by: Ramakrishna Prabhu --- .../mathematical_optimization/constants.h | 6 ++-- .../cuopt/mathematical_optimization/cuopt_c.h | 22 +++++++----- .../c_api_tests/c_api_tests.cpp | 35 ++++++++++++++----- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 28d9bd3733..8747a320c9 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -277,9 +277,9 @@ * range from the problem selectors so a problem selector passed to a solution accessor, or the * reverse, is rejected rather than silently read. * - * LP attributes require an LP solution and MIP attributes require a MIP solution; the accessors - * return CUOPT_INVALID_ARGUMENT otherwise. Use CUOPT_ATTR_IS_MIP on the originating problem to - * tell which set applies. + * Which of these a solution carries depends on the class of problem that produced it; the + * accessors return CUOPT_INVALID_ARGUMENT for one that does not apply. Use CUOPT_ATTR_IS_MIP on + * the originating problem to determine the class. */ #define CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL 300 #define CUOPT_SOLUTION_ATTR_LP_DUAL_RESIDUAL 301 diff --git a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h index 15f4aa4f51..c133ab8f8e 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -1115,13 +1115,19 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ /* -------------------------------------------------------------------------- */ /* - * Solver statistics are read as scalar attributes, using the CUOPT_SOLUTION_ATTR_* selectors in - * constants.h. New statistics can then be added as constants rather than as new exported - * functions, which keeps the ABI stable for existing callers. + * A solution attribute is a read-only value describing a completed solve, selected by one of the + * CUOPT_SOLUTION_ATTR_* integer constants in constants.h and passed as cuopt_int_t. The + * attributes available here are solver statistics: residuals, gap, iteration and node counts, + * presolve time, and violation magnitudes. * - * LP and MIP statistics come from different solvers, so an LP selector requires an LP solution - * and a MIP selector requires a MIP solution. Query CUOPT_ATTR_IS_MIP on the originating problem - * to decide which set applies. + * Attributes are distinct from parameters. A parameter is an input, set on a cuOptSolverSettings + * before solving with cuOptSetParameter and read back with cuOptGetParameter. An attribute is an + * output, read from a solved cuOptSolution, or from a cuOptOptimizationProblem in the case of the + * problem attributes further below. + * + * Not every attribute applies to every solution: which statistics a solve produces depends on the + * class of problem it was given. An attribute that does not apply returns CUOPT_INVALID_ARGUMENT. + * Use CUOPT_ATTR_IS_MIP on the originating problem to determine the class. */ /** @brief Get a scalar integer solution attribute (a CUOPT_SOLUTION_ATTR_* with an integer @@ -1135,7 +1141,7 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ * * @return A status code indicating success or failure. Returns CUOPT_INVALID_ARGUMENT if the * selector is unknown, does not have an integer value, or does not apply to this solution's - * solver. + * problem class. */ cuopt_int_t cuOptGetSolutionIntAttribute(cuOptSolution solution, cuopt_int_t attribute, @@ -1152,7 +1158,7 @@ cuopt_int_t cuOptGetSolutionIntAttribute(cuOptSolution solution, * * @return A status code indicating success or failure. Returns CUOPT_INVALID_ARGUMENT if the * selector is unknown, does not have a floating-point value, or does not apply to this - * solution's solver. + * solution's problem class. */ cuopt_int_t cuOptGetSolutionFloatAttribute(cuOptSolution solution, cuopt_int_t attribute, 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 12cf870e67..e1a11057ef 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 @@ -1089,6 +1089,25 @@ TEST(c_api, problem_attributes_names) namespace { +// Destroys the solution however the test leaves scope. The checks below use ASSERT, which +// returns early on failure, so an explicit destroy at the end of the test would be skipped +// exactly when a test fails and leak the solution into the rest of the binary. +class scoped_solution_t { + public: + explicit scoped_solution_t(cuOptSolution solution) : solution_(solution) {} + ~scoped_solution_t() + { + if (solution_ != nullptr) { cuOptDestroySolution(&solution_); } + } + scoped_solution_t(const scoped_solution_t&) = delete; + scoped_solution_t& operator=(const scoped_solution_t&) = delete; + + cuOptSolution get() const { return solution_; } + + private: + cuOptSolution solution_; +}; + // Builds and solves a two-variable problem, integral when `mip` is set. cuOptSolution solve_tiny_problem(bool mip) { @@ -1132,8 +1151,10 @@ cuOptSolution solve_tiny_problem(bool mip) TEST(c_api, lp_solution_attributes) { - cuOptSolution solution = solve_tiny_problem(false); - ASSERT_NE(solution, nullptr); + cuOptSolution raw_solution = solve_tiny_problem(false); + ASSERT_NE(raw_solution, nullptr); + scoped_solution_t scoped(raw_solution); + cuOptSolution solution = scoped.get(); // Seed with NaN rather than a numeric sentinel: the solver cannot legitimately report NaN, // so "still NaN" means the accessor never wrote the value. A numeric sentinel would be @@ -1179,14 +1200,14 @@ TEST(c_api, lp_solution_attributes) CUOPT_INVALID_ARGUMENT); EXPECT_EQ(cuOptGetSolutionFloatAttribute(nullptr, CUOPT_SOLUTION_ATTR_LP_GAP, &as_float), CUOPT_INVALID_ARGUMENT); - - cuOptDestroySolution(&solution); } TEST(c_api, mip_solution_attributes) { - cuOptSolution solution = solve_tiny_problem(true); - ASSERT_NE(solution, nullptr); + cuOptSolution raw_solution = solve_tiny_problem(true); + ASSERT_NE(raw_solution, nullptr); + scoped_solution_t scoped(raw_solution); + cuOptSolution solution = scoped.get(); // Violations are magnitudes, so they cannot be negative. for (cuopt_int_t attribute : {CUOPT_SOLUTION_ATTR_MIP_PRESOLVE_TIME, @@ -1212,6 +1233,4 @@ TEST(c_api, mip_solution_attributes) cuopt_float_t as_float = 0; EXPECT_EQ(cuOptGetSolutionFloatAttribute(solution, CUOPT_SOLUTION_ATTR_LP_GAP, &as_float), CUOPT_INVALID_ARGUMENT); - - cuOptDestroySolution(&solution); } From 7a43536a56f3747e5fb4f788f816be81a52f442b Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 13 Aug 2026 16:50:21 -0500 Subject: [PATCH 3/3] Dispatch solution attributes per case rather than by selector range is_lp_solution_attribute decided whether a selector was LP or MIP by testing whether its value fell between CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL and CUOPT_SOLUTION_ATTR_LP_SOLVED_BY. That made the numeric values load-bearing: a new LP attribute would either have to be inserted inside that range, renumbering the constants after it and breaking anyone compiled against them, or be appended after the MIP block where the range test would classify it as MIP. Replace it with the per-case macros mlubin suggested. Each case names the kind of solution it reads, so selector values carry no meaning beyond identity and new selectors can be appended anywhere. Behaviour is unchanged. A selector that names the other kind of solution still returns CUOPT_INVALID_ARGUMENT, now because the corresponding accessor yields nullptr rather than because of a range test, and a selector of the wrong value type falls through to default. Both remain covered by the existing tests. Also trims the selector comment in constants.h, per review. Verified: 69/69 C_API_TEST cases pass. Signed-off-by: Ramakrishna Prabhu --- .../mathematical_optimization/constants.h | 3 +- cpp/src/pdlp/cuopt_c.cpp | 101 ++++++++---------- 2 files changed, 44 insertions(+), 60 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 8747a320c9..a4e9e30a4b 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -274,8 +274,7 @@ /* @brief Scalar solution attribute selectors * (see cuOptGetSolution{Int,Float}Attribute). Passed as cuopt_int_t; numbered in a separate - * range from the problem selectors so a problem selector passed to a solution accessor, or the - * reverse, is rejected rather than silently read. + * range from the problem selectors. * * Which of these a solution carries depends on the class of problem that produced it; the * accessors return CUOPT_INVALID_ARGUMENT for one that does not apply. Use CUOPT_ATTR_IS_MIP on diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 3614c8d482..95399b673b 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -1368,13 +1368,7 @@ cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_ namespace { // Solution attribute plumbing. Each selector names one scalar on the LP or MIP solution -// interface; adding a statistic later means adding a constant and one case, not a new symbol. - -bool is_lp_solution_attribute(cuopt_int_t attribute) -{ - return attribute >= CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL && - attribute <= CUOPT_SOLUTION_ATTR_LP_SOLVED_BY; -} +// interface; adding a statistic later means adding a constant and one line, not a new symbol. lp_solution_interface_t* as_lp_solution(cuOptSolution solution) { @@ -1390,6 +1384,24 @@ mip_solution_interface_t* as_mip_solution(cuOptSolut } // namespace +// Each case states which kind of solution it reads, so a selector's numeric value carries no +// meaning beyond identity and new selectors can be appended anywhere. +#define CUOPT_READ_LP_ATTRIBUTE(selector, getter, cast_to) \ + case selector: { \ + auto* lp = as_lp_solution(solution); \ + if (lp == nullptr) { return CUOPT_INVALID_ARGUMENT; } \ + *value_out = static_cast(lp->getter()); \ + return CUOPT_SUCCESS; \ + } + +#define CUOPT_READ_MIP_ATTRIBUTE(selector, getter, cast_to) \ + case selector: { \ + auto* mip = as_mip_solution(solution); \ + if (mip == nullptr) { return CUOPT_INVALID_ARGUMENT; } \ + *value_out = static_cast(mip->getter()); \ + return CUOPT_SUCCESS; \ + } + cuopt_int_t cuOptGetSolutionIntAttribute(cuOptSolution solution, cuopt_int_t attribute, cuopt_int_t* value_out) @@ -1398,29 +1410,13 @@ cuopt_int_t cuOptGetSolutionIntAttribute(cuOptSolution solution, if (value_out == nullptr) { return CUOPT_INVALID_ARGUMENT; } try { - if (is_lp_solution_attribute(attribute)) { - auto* lp = as_lp_solution(solution); - if (lp == nullptr) { return CUOPT_INVALID_ARGUMENT; } - switch (attribute) { - case CUOPT_SOLUTION_ATTR_LP_NUM_ITERATIONS: - *value_out = static_cast(lp->get_num_iterations()); - return CUOPT_SUCCESS; - case CUOPT_SOLUTION_ATTR_LP_SOLVED_BY: - *value_out = static_cast(lp->solved_by()); - return CUOPT_SUCCESS; - default: return CUOPT_INVALID_ARGUMENT; // a float-valued LP selector - } - } - - auto* mip = as_mip_solution(solution); - if (mip == nullptr) { return CUOPT_INVALID_ARGUMENT; } switch (attribute) { - case CUOPT_SOLUTION_ATTR_MIP_NUM_NODES: - *value_out = static_cast(mip->get_num_nodes()); - return CUOPT_SUCCESS; - case CUOPT_SOLUTION_ATTR_MIP_NUM_SIMPLEX_ITERATIONS: - *value_out = static_cast(mip->get_num_simplex_iterations()); - return CUOPT_SUCCESS; + CUOPT_READ_LP_ATTRIBUTE( + CUOPT_SOLUTION_ATTR_LP_NUM_ITERATIONS, get_num_iterations, cuopt_int_t) + CUOPT_READ_LP_ATTRIBUTE(CUOPT_SOLUTION_ATTR_LP_SOLVED_BY, solved_by, cuopt_int_t) + CUOPT_READ_MIP_ATTRIBUTE(CUOPT_SOLUTION_ATTR_MIP_NUM_NODES, get_num_nodes, cuopt_int_t) + CUOPT_READ_MIP_ATTRIBUTE( + CUOPT_SOLUTION_ATTR_MIP_NUM_SIMPLEX_ITERATIONS, get_num_simplex_iterations, cuopt_int_t) default: return CUOPT_INVALID_ARGUMENT; } } catch (const std::exception& e) { @@ -1436,36 +1432,22 @@ cuopt_int_t cuOptGetSolutionFloatAttribute(cuOptSolution solution, if (value_out == nullptr) { return CUOPT_INVALID_ARGUMENT; } try { - if (is_lp_solution_attribute(attribute)) { - auto* lp = as_lp_solution(solution); - if (lp == nullptr) { return CUOPT_INVALID_ARGUMENT; } - switch (attribute) { - case CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL: - *value_out = lp->get_l2_primal_residual(); - return CUOPT_SUCCESS; - case CUOPT_SOLUTION_ATTR_LP_DUAL_RESIDUAL: - *value_out = lp->get_l2_dual_residual(); - return CUOPT_SUCCESS; - case CUOPT_SOLUTION_ATTR_LP_GAP: *value_out = lp->get_gap(); return CUOPT_SUCCESS; - default: return CUOPT_INVALID_ARGUMENT; // an int-valued LP selector - } - } - - auto* mip = as_mip_solution(solution); - if (mip == nullptr) { return CUOPT_INVALID_ARGUMENT; } switch (attribute) { - case CUOPT_SOLUTION_ATTR_MIP_PRESOLVE_TIME: - *value_out = mip->get_presolve_time(); - return CUOPT_SUCCESS; - case CUOPT_SOLUTION_ATTR_MIP_MAX_CONSTRAINT_VIOLATION: - *value_out = mip->get_max_constraint_violation(); - return CUOPT_SUCCESS; - case CUOPT_SOLUTION_ATTR_MIP_MAX_INT_VIOLATION: - *value_out = mip->get_max_int_violation(); - return CUOPT_SUCCESS; - case CUOPT_SOLUTION_ATTR_MIP_MAX_VARIABLE_BOUND_VIOLATION: - *value_out = mip->get_max_variable_bound_violation(); - return CUOPT_SUCCESS; + CUOPT_READ_LP_ATTRIBUTE( + CUOPT_SOLUTION_ATTR_LP_PRIMAL_RESIDUAL, get_l2_primal_residual, cuopt_float_t) + CUOPT_READ_LP_ATTRIBUTE( + CUOPT_SOLUTION_ATTR_LP_DUAL_RESIDUAL, get_l2_dual_residual, cuopt_float_t) + CUOPT_READ_LP_ATTRIBUTE(CUOPT_SOLUTION_ATTR_LP_GAP, get_gap, cuopt_float_t) + CUOPT_READ_MIP_ATTRIBUTE( + CUOPT_SOLUTION_ATTR_MIP_PRESOLVE_TIME, get_presolve_time, cuopt_float_t) + CUOPT_READ_MIP_ATTRIBUTE(CUOPT_SOLUTION_ATTR_MIP_MAX_CONSTRAINT_VIOLATION, + get_max_constraint_violation, + cuopt_float_t) + CUOPT_READ_MIP_ATTRIBUTE( + CUOPT_SOLUTION_ATTR_MIP_MAX_INT_VIOLATION, get_max_int_violation, cuopt_float_t) + CUOPT_READ_MIP_ATTRIBUTE(CUOPT_SOLUTION_ATTR_MIP_MAX_VARIABLE_BOUND_VIOLATION, + get_max_variable_bound_violation, + cuopt_float_t) default: return CUOPT_INVALID_ARGUMENT; } } catch (const std::exception& e) { @@ -1473,6 +1455,9 @@ cuopt_int_t cuOptGetSolutionFloatAttribute(cuOptSolution solution, } } +#undef CUOPT_READ_LP_ATTRIBUTE +#undef CUOPT_READ_MIP_ATTRIBUTE + /* -------------------------------------------------------------------------- */ /* Generic problem attribute getters */ /* -------------------------------------------------------------------------- */