Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 20 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,24 @@
#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.
*
* 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
#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
54 changes: 54 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,60 @@ 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.

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

/*
* 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.
*
* 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
* 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
* problem class.
*/
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 problem class.
*/
cuopt_int_t cuOptGetSolutionFloatAttribute(cuOptSolution solution,
cuopt_int_t attribute,
cuopt_float_t* value_out);

/* -------------------------------------------------------------------------- */
/* Generic problem attributes */
/* -------------------------------------------------------------------------- */
Expand Down
96 changes: 96 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,99 @@ 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 line, not a new symbol.

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

// 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<cast_to>(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<cast_to>(mip->getter()); \
return CUOPT_SUCCESS; \
}

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 {
switch (attribute) {
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) {
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 {
switch (attribute) {
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) {
return CUOPT_RUNTIME_ERROR;
}
}

#undef CUOPT_READ_LP_ATTRIBUTE
#undef CUOPT_READ_MIP_ATTRIBUTE

/* -------------------------------------------------------------------------- */
/* Generic problem attribute getters */
/* -------------------------------------------------------------------------- */
Expand Down
157 changes: 157 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,158 @@ 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 {

// 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)
{
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 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
// 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);
}

TEST(c_api, mip_solution_attributes)
{
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,
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);
}
Loading