Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cpp/include/cuopt/mathematical_optimization/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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.

*
* 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
48 changes: 48 additions & 0 deletions cpp/include/cuopt/mathematical_optimization/cuopt_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,54 @@ cuopt_int_t cuOptGetDualObjectiveValue(cuOptSolution solution,
*/
cuopt_int_t cuOptGetReducedCosts(cuOptSolution solution, cuopt_float_t* reduced_cost_ptr);

/* -------------------------------------------------------------------------- */
/* Solution attributes */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're introducing a new concept of a solution attribute without a clearly documented distinction on what makes them different from parameters.

/* -------------------------------------------------------------------------- */

/*
* 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment should describe what the code does, not what alternative designs could have been.

* 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever present "LP" and "MIP" as different solvers in the C API? It sounds like we're leaking an implementation detail. How about simply saying that not all attributes are present for all solutions, depending on the problem class?

* 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 */
/* -------------------------------------------------------------------------- */
Expand Down
111 changes: 111 additions & 0 deletions cpp/src/pdlp/cuopt_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <cuopt/version_config.hpp>

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <span>
Expand All @@ -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;
Expand Down Expand Up @@ -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 &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach doesn't leave any room for us to define new LP solution attributes in the future. Maybe we don't really need is_lp_solution_attribute and can instead define macros like:

PROCESS_MIP_INT_ATTR(CUOPT_SOLUTION_ATTR_MIP_NUM_NODES, get_num_nodes)

which translates to:

case CUOPT_SOLUTION_ATTR_MIP_NUM_NODES:
    auto* mip = as_mip_solution(solution);
    if (mip == nullptr) { return CUOPT_INVALID_ARGUMENT; }
    *value_out = static_cast<cuopt_int_t>(mip->get_num_nodes());
    return CUOPT_SUCCESS;

attribute <= CUOPT_SOLUTION_ATTR_LP_SOLVED_BY;
}

lp_solution_interface_t<cuopt_int_t, cuopt_float_t>* as_lp_solution(cuOptSolution solution)
{
auto* view = static_cast<solution_and_stream_view_t*>(solution);
return view->is_mip ? nullptr : view->lp_solution_interface_ptr;
}

mip_solution_interface_t<cuopt_int_t, cuopt_float_t>* as_mip_solution(cuOptSolution solution)
{
auto* view = static_cast<solution_and_stream_view_t*>(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<cuopt_int_t>(lp->get_num_iterations());
return CUOPT_SUCCESS;
case CUOPT_SOLUTION_ATTR_LP_SOLVED_BY:
*value_out = static_cast<cuopt_int_t>(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<cuopt_int_t>(mip->get_num_nodes());
return CUOPT_SUCCESS;
case CUOPT_SOLUTION_ATTR_MIP_NUM_SIMPLEX_ITERATIONS:
*value_out = static_cast<cuopt_int_t>(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 */
/* -------------------------------------------------------------------------- */
Expand Down
138 changes: 138 additions & 0 deletions cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

#include "c_api_tests.h"

#include <cmath>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
Expand Down Expand Up @@ -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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<< "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);
}
Loading