diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py index 39460436e3..1ef73669d3 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py @@ -6,6 +6,8 @@ This example demonstrates: - Using callbacks to receive intermediate solutions during MIP solving +- Using Problem.getIncumbentValues() to extract variable values from + incumbent solutions - Tracking solution progress as the solver improves the solution - Accessing incumbent (best so far) solutions before final optimum - Custom callback class implementation @@ -26,12 +28,11 @@ x, y are integers Expected Output: - Incumbent 1: [ 0. 58.], cost: 174.00 - Incumbent 2: [36. 41.], cost: 303.00 + Incumbent 1: x=36.0, y=41.0, cost: 303.00 === Final Results === Problem status: Optimal - Solve time: 0.16 seconds + Solve time: 0.27 seconds Final solution: x=36.0, y=41.0 Final objective value: 303.00 """ @@ -42,59 +43,52 @@ from cuopt.linear_programming.internals import GetSolutionCallback -# Create a callback class to receive incumbent solutions class IncumbentCallback(GetSolutionCallback): - """Callback to receive and track incumbent solutions during solving.""" + """Callback to receive and track incumbent solutions during solving. - def __init__(self, user_data): + Uses Problem.getIncumbentValues() to extract variable values from the + raw incumbent solution array. + """ + + def __init__(self, problem, variables, user_data): super().__init__() + self.problem = problem + self.variables = variables self.solutions = [] self.n_callbacks = 0 self.user_data = user_data def get_solution(self, solution, solution_cost, solution_bound, user_data): + """Called whenever the solver finds a new incumbent solution.""" assert user_data is self.user_data - """ - Called whenever the solver finds a new incumbent solution. - - Parameters - ---------- - solution : array-like - The variable values of the incumbent solution - solution_cost : array-like - The objective value of the incumbent solution - solution_bound : array-like - The current best bound in user objective space - """ self.n_callbacks += 1 - # Store the incumbent solution + # Use getIncumbentValues to extract values for specific variables + values = self.problem.getIncumbentValues(solution, self.variables) + incumbent = { - "solution": solution.tolist(), + "values": values, "cost": float(solution_cost[0]), "bound": float(solution_bound[0]), "iteration": self.n_callbacks, - "user_data": user_data, } self.solutions.append(incumbent) - print( - f"Incumbent {self.n_callbacks}: {incumbent['solution']}, " - f"cost: {incumbent['cost']:.2f}" - ) + print(f"Incumbent {self.n_callbacks}:", end=" ") + for i, var in enumerate(self.variables): + print(f"{var.VariableName}={values[i]}", end=" ") + print(f"cost: {incumbent['cost']:.2f}") def main(): """Run the incumbent solutions example.""" - # Create a more complex MIP problem that will generate multiple incumbents problem = Problem("Incumbent Example") # Add integer variables - x = problem.addVariable(vtype=INTEGER) - y = problem.addVariable(vtype=INTEGER) + x = problem.addVariable(vtype=INTEGER, name="x") + y = problem.addVariable(vtype=INTEGER, name="y") - # Add constraints to create a problem that will generate multiple - # incumbents + # Add constraints problem.addConstraint(2 * x + 4 * y >= 230) problem.addConstraint(3 * x + 2 * y <= 190) @@ -103,11 +97,9 @@ def main(): # Configure solver settings with callback settings = SolverSettings() - # Set the incumbent callback user_data = {"source": "incumbent_solutions_example"} - incumbent_callback = IncumbentCallback(user_data) + incumbent_callback = IncumbentCallback(problem, [x, y], user_data) settings.set_mip_callback(incumbent_callback, user_data) - # Allow enough time to find multiple incumbents settings.set_parameter(CUOPT_TIME_LIMIT, 30) # Solve the problem @@ -117,10 +109,11 @@ def main(): print("\n=== Final Results ===") print(f"Problem status: {problem.Status.name}") print(f"Solve time: {problem.SolveTime:.2f} seconds") - print(f"Final solution: x={x.getValue()}, y={y.getValue()}") - print(f"Final objective value: {problem.ObjValue:.2f}") + print("Final solution: ", end=" ") + for i, var in enumerate(problem.getVariables()): + print(f"{var.VariableName}={var.getValue()} ", end=" ") + print(f"\nFinal objective value: {problem.ObjValue:.2f}") - # Display all incumbents found print( f"\nTotal incumbent solutions found: " f"{len(incumbent_callback.solutions)}" 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..2edd625fbc 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 @@ -167,18 +167,17 @@ The response is as follows: .. code-block:: text Optimal solution found. - Incumbent 1: [ 0. 58.], cost: 174.00 - Incumbent 2: [36. 41.], cost: 303.00 - Generated fast solution in 0.158467 seconds with objective 303.000000 - Consuming B&B solutions, solution queue size 2 - Solution objective: 303.000000 , relative_mip_gap 0.000000 solution_bound 303.000000 presolve_time 0.043211 total_solve_time 0.160270 max constraint violation 0.000000 max int violation 0.000000 max var bounds violation 0.000000 nodes 4 simplex_iterations 3 + Incumbent 1: x=36.0 y=41.0 cost: 303.00 + Solution objective: 303.000000 , relative_mip_gap 0.000000 solution_bound 303.000000 presolve_time 0.103659 total_solve_time 0.173678 max constraint violation 0.000000 max int violation 0.000000 max var bounds violation 0.000000 nodes 0 simplex_iterations 2 === Final Results === Problem status: Optimal - Solve time: 0.16 seconds - Final solution: x=36.0, y=40.99999999999999 + Solve time: 0.17 seconds + Final solution: x=36.0 y=41.0 Final objective value: 303.00 + Total incumbent solutions found: 1 + Working with PDLP Warmstart Data -------------------------------- diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 5821fd3a93..5976ee7bb0 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1703,7 +1703,16 @@ def updateObjective(self, coeffs=[], constant=None, sense=None): def getIncumbentValues(self, solution, vars): """ - Extract incumbent values of the vars from a problem solution. + This is a utility function that can be used for extracting incumbent values + of the given variables during a Solve using the incumbent callback. + Please check docs for more details and examples of incumbent callbacks. + + Parameters + ---------- + solution : List[float] + Array-like structure containing incumbent values. + vars : List[:py:class:`Variable`] + List of variables to extract corresponding incumbent values. """ values = [] for var in vars: diff --git a/skills/cuopt-lp-milp-api-python/assets/milp_basic/incumbent_callback.py b/skills/cuopt-lp-milp-api-python/assets/milp_basic/incumbent_callback.py index 49e533291c..38f553f7e1 100644 --- a/skills/cuopt-lp-milp-api-python/assets/milp_basic/incumbent_callback.py +++ b/skills/cuopt-lp-milp-api-python/assets/milp_basic/incumbent_callback.py @@ -13,20 +13,19 @@ class IncumbentCallback(GetSolutionCallback): - def __init__(self, user_data): + def __init__(self, problem, variables, user_data): super().__init__() + self.problem = problem + self.variables = variables self.n_callbacks = 0 self.user_data = user_data def get_solution(self, solution, solution_cost, solution_bound, user_data): self.n_callbacks += 1 - sol = ( - solution.tolist() - if hasattr(solution, "tolist") - else list(solution) - ) + values = self.problem.getIncumbentValues(solution, self.variables) cost = float(solution_cost[0]) - print(f"Incumbent {self.n_callbacks}: {sol}, cost: {cost:.2f}") + vals_str = ", ".join(f"{float(v)}" for v in values) + print(f"Incumbent {self.n_callbacks}: [{vals_str}], cost: {cost:.2f}") def main(): @@ -39,7 +38,8 @@ def main(): user_data = {"source": "incumbent_callback"} settings = SolverSettings() - settings.set_mip_callback(IncumbentCallback(user_data), user_data) + callback = IncumbentCallback(problem, [x, y], user_data) + settings.set_mip_callback(callback, user_data) settings.set_parameter(CUOPT_TIME_LIMIT, 30) problem.solve(settings)