diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/problem_solution_api_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/problem_solution_api_example.py new file mode 100644 index 0000000000..11426ff7ec --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/problem_solution_api_example.py @@ -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() diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst index a3412d1a82..f4251f78c0 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst @@ -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() `, you can retrieve the solution object with :py:meth:`getSolution() ` and pass it to :py:meth:`getIncumbentValues() ` together with :py:meth:`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 ` + +.. 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 -------------------------------- diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index baf9716191..0c12965eae 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -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 @@ -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( @@ -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." @@ -1926,6 +1964,7 @@ 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()): @@ -1933,6 +1972,9 @@ 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") @@ -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() diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py index 19db315349..867c0f0a7a 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py @@ -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 ----------