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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Comment on lines +31 to 37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Docstring expected output formatting is out of sync with actual prints.

The expected output uses comma-separated formatting (x=..., y=...), but the callback/final print blocks output space-separated pairs. Please align the docstring output with runtime output to avoid confusion.

✏️ Suggested docstring sync
-    Incumbent 1: x=36.0, y=41.0, cost: 303.00
+    Incumbent 1: x=36.0 y=41.0 cost: 303.00
...
-    Final solution: x=36.0, y=41.0
+    Final solution:  x=36.0  y=41.0

As per coding guidelines, docs/**/* changes should prioritize accuracy and consistency between examples and behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py`
around lines 31 - 37, Update the docstring expected output to match the runtime
prints: replace comma-separated "x=..., y=..." lines in the example output with
the space-separated pairs produced by the callback and final print blocks (e.g.,
align the "Incumbent 1: ..." and "Final solution: ..." lines to use the same
space-separated formatting shown at runtime); locate the output strings in the
example file (incumbent_solutions_example.py) where the docstring contains the
expected output block and make the formatting consistent with the actual prints
emitted by the callback and final print statements.

"""
Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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)}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------------------

Expand Down
11 changes: 10 additions & 1 deletion python/cuopt/cuopt/linear_programming/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)

Expand Down
Loading