Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""
Problem Solution API Example

This example demonstrates how to use the Problem API to access the solution
after solve(): getSolution() and getIncumbentValues().

- getSolution() returns the Solution object from the last solve (None before
solve or after the problem is modified).
- getIncumbentValues(solution, vars) returns the primal values for the
given variables from that solution. Use it with getSolution() and
getVariables() to get values in a list, or for a subset of variables.

Problem:
Maximize: x + y
Subject to:
x + y <= 10
x - y >= 0
x, y >= 0

Expected Output:
Optimal solution found in 0.01 seconds
Objective: 10.0
Values via var.Value: x=10.0, y=0.0
Values via getIncumbentValues: [10.0, 0.0]
Subset (x only) via getIncumbentValues: [10.0]
"""

from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE
from cuopt.linear_programming.solver_settings import SolverSettings


def main():
"""Run the Problem solution API example."""
problem = Problem("Solution API Example")

x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x")
y = problem.addVariable(lb=0, vtype=CONTINUOUS, name="y")

problem.addConstraint(x + y <= 10, name="c1")
problem.addConstraint(x - y >= 0, name="c2")
problem.setObjective(x + y, sense=MAXIMIZE)

settings = SolverSettings()
problem.solve(settings)

if problem.Status.name == "Optimal":
print(f"Optimal solution found in {problem.SolveTime:.2f} seconds")
print(f"Objective: {problem.ObjValue}")

# Access values via variable attributes (populated by solve)
print(
f"Values via var.Value: x={x.getValue()}, y={y.getValue()}"
)

# Access solution via getSolution() and getIncumbentValues()
solution = problem.getSolution()
vars_list = problem.getVariables()
values = problem.getIncumbentValues(solution, vars_list)
print(f"Values via getIncumbentValues: {values}")

# getIncumbentValues works with a subset of variables too
values_x_only = problem.getIncumbentValues(solution, [x])
print(f"Subset (x only) via getIncumbentValues: {values_x_only}")
else:
print(f"Problem status: {problem.Status.name}")


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,27 @@ The response is as follows:
c1 DualValue = 1.0000000592359144
c2 DualValue = 1.0000000821854418

Accessing the Solution with getSolution() and getIncumbentValues()
------------------------------------------------------------------

After calling :py:meth:`solve() <cuopt.linear_programming.problem.Problem.solve>`, you can retrieve the solution object with :py:meth:`getSolution() <cuopt.linear_programming.problem.Problem.getSolution>` and pass it to :py:meth:`getIncumbentValues() <cuopt.linear_programming.problem.Problem.getIncumbentValues>` together with :py:meth:`getVariables() <cuopt.linear_programming.problem.Problem.getVariables>` to get primal values as a list (e.g. for a subset of variables or a specific order).

:download:`problem_solution_api_example.py <examples/problem_solution_api_example.py>`

.. literalinclude:: examples/problem_solution_api_example.py
:language: python
:linenos:

The response is as follows:

.. code-block:: text

Optimal solution found in 0.01 seconds
Objective: 10.0
Values via var.Value: x=10.0, y=0.0
Values via getIncumbentValues: [10.0, 0.0]
Subset (x only) via getIncumbentValues: [10.0]

Working with Incumbent Solutions
--------------------------------

Expand Down
45 changes: 45 additions & 0 deletions python/cuopt/cuopt/linear_programming/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,7 @@ def __init__(self, model_name=""):
self.ObjConstant = 0.0
self.Status = -1
self.warmstart_data = None
self.solution = None

self.model = None
self.solved = False
Expand Down Expand Up @@ -1474,6 +1475,7 @@ def reset_solved_values(self):
self.constraint_csr_matrix = None
self.objective_qmatrix = None
self.warmstart_data = None
self.solution = None
self.solved = False

def addVariable(
Expand Down Expand Up @@ -1694,12 +1696,48 @@ def updateObjective(self, coeffs=[], constant=None, sense=None):
def getIncumbentValues(self, solution, vars):
"""
Extract incumbent values of the vars from a problem solution.

Use with the Problem API by passing the solution from :py:meth:`getSolution`
(after :py:meth:`solve`), and the variables from :py:meth:`getVariables`.
When using a MIP incumbent callback (:py:meth:`cuopt.linear_programming.solver_settings.SolverSettings.set_mip_callback`),
you can pass the callback's ``solution`` argument (array-like, indexed by
variable index) to get values for your variables.

Parameters
----------
solution : Solution or array-like
Either the Solution from :py:meth:`getSolution`, or a primal
solution array (e.g. from GetSolutionCallback.get_solution)
indexable by variable index.
vars : list of Variable
Variables to extract values for (e.g. from :py:meth:`getVariables`).

Returns
-------
list of float
Incumbent values for the given variables, in the same order as vars.
"""
values = []
for var in vars:
values.append(solution[var.index])
return values

def getSolution(self):
"""
Return the solution from the last solve.

Set after :py:meth:`solve` completes; ``None`` before solve or after
the problem is modified (e.g. addVariable, addConstraint, setObjective).
Can be passed to :py:meth:`getIncumbentValues` together with
:py:meth:`getVariables`.

Returns
-------
solution : Solution or None
The last solution object, or None.
"""
return self.solution

def get_incumbent_values(self, solution, vars):
warnings.warn(
"This function is deprecated and will be removed."
Expand Down Expand Up @@ -1926,13 +1964,17 @@ def populate_solution(self, solution):
if dual_sol is not None and len(dual_sol) > 0:
constr.DualValue = dual_sol[i]
constr.Slack = constr.compute_slack()
self.solution = solution
self.solved = True

def solve(self, settings=solver_settings.SolverSettings()):
"""
Optimizes the LP or MIP problem with the added variables,
constraints and objective.

The solution is stored on the problem (see :py:meth:`getSolution`).
Variable values are populated on each variable's ``Value`` attribute.

Examples
--------
>>> problem = problem.Problem("MIP_model")
Expand All @@ -1943,6 +1985,9 @@ def solve(self, settings=solver_settings.SolverSettings()):
>>> problem.addConstraint(expr + x == 20, name="Constr2")
>>> problem.setObjective(x + y, sense=MAXIMIZE)
>>> problem.solve()
>>> values = problem.getIncumbentValues(
... problem.getSolution(), problem.getVariables()
... )
"""
if self.model is None:
self._to_data_model()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,12 @@ def set_mip_callback(self, callback, user_data):
"""
Note: Only supported for MILP

Set the callback to receive incumbent solution.
Set the callback to receive incumbent solution. The ``solution``
passed to your callback is indexable by variable index and can be
used with :py:meth:`cuopt.linear_programming.problem.Problem.getIncumbentValues`
to get values for specific variables (e.g. pass the problem in
user_data and call ``problem.getIncumbentValues(solution,
problem.getVariables())``).

Parameters
----------
Expand Down