Search code examples
or-toolsmixed-integer-programmingcp-sat

ortools - CP-SAT : how to fetch the feasible solution if the best incumbent solution does not change after 'x' seconds


I am solving integer programming problem leveraging CP-SAT solver (ortools). Looking at the logs, I notice that the solver finds the solution that actually is optimal (proven later), but spends a lot of time in proving optimality. Essentially, the gap between best incumbent solution and the best bound closes down slowly.

I tried outputting the feasible solution if the MIP gap % : (abs(objective - best_bound) / objective) < 0.05, but it did not help. Since the convergence is slow.

from ortools.sat.python import cp_model as cp
import time

class PrinterClass(cp.CpSolverSolutionCallback):

    def __init__(self):
        cp.CpSolverSolutionCallback.__init__(self)
        self.__solution_count = 0
        self.__start_time = time.time()

    def on_solution_callback(self):
        current_time = time.time()
        objective = self.ObjectiveValue()
        best_bound = self.BestObjectiveBound()
        print("Solution %i, time = %f s, objective = %i, best_bound = %i" %
              (self.__solution_count, current_time - self.__start_time,
               objective, best_bound))
        self.__solution_count += 1
        
        if (abs(objective - best_bound) / objective) < 0.05:
            print('Stop search')
            self.StopSearch()
            
    def total_num_solutions(self):
        return self.__solution_count

What I want to try is : if the best incumbent solution does not change for 10 seconds (say), then I want to output this particular feasible solution and stop the search. Instead of solver spending 200 seconds (say) trying to close the gap between the incumbent solution and the best bound.

E.g. if you look at the log below, the solver finds a feasible solution (that is actually optimal, proven later), solution #13 at 2.75 secs,

Solution 13, time = 2.745947 s, objective = 2032, best_bound = 1479

and then spends ~25 secs more to prove that its actually optimal, can I stop the search and get the solution when the solver spends additional 10 secs and is not able to improve upon the current incumbent solution. At below point

#Bound  12.14s best:2032  next:[1532,2030] pseudo_costs

I just want to try this out, at least for my learning, can somebody help with this ?

Below is the log generated :

Starting Search at 0.28s with 8 workers.
6 full subsolvers: [default_lp, no_lp, max_lp, reduced_costs, pseudo_costs, quick_restart]
Interleaved subsolvers: [feasibility_pump, rnd_var_lns_default, rnd_cst_lns_default, graph_var_lns_default, graph_cst_lns_default, routing_random_lns_default, routing_path_lns_default, routing_full_path_lns_default, rins_lns_default, rens_lns_default]
#1       0.44s best:4884  next:[0,4883]   no_lp fixed_bools:1/931
Solution 0, time = 0.435596 s, objective = 4884, best_bound = 0
#Bound   0.45s best:4884  next:[0,4882]   no_lp
#2       0.46s best:4336  next:[0,4335]   no_lp fixed_bools:1/935
Solution 1, time = 0.466841 s, objective = 4336, best_bound = 0
#Bound   0.47s best:4336  next:[0,4334]   no_lp
#Bound   0.48s best:4336  next:[1278,4334] max_lp initial_propagation
#3       0.54s best:3972  next:[1278,3971] no_lp fixed_bools:1/939
Solution 2, time = 0.544969 s, objective = 3972, best_bound = 1278
#Bound   0.55s best:3972  next:[1278,3970] no_lp
#4       0.55s best:3492  next:[1278,3491] routing_random_lns_default(d=0.50 s=12 t=0.10 p=0.00)
Solution 3, time = 0.560597 s, objective = 3492, best_bound = 1278
#Bound   0.56s best:3492  next:[1278,3490] no_lp
#5       0.57s best:2968  next:[1278,2967] no_lp fixed_bools:1/944
Solution 4, time = 0.576220 s, objective = 2968, best_bound = 1278
#Bound   0.58s best:2968  next:[1278,2966] no_lp
#6       0.62s best:2944  next:[1278,2943] no_lp fixed_bools:1/949
Solution 5, time = 0.632293 s, objective = 2944, best_bound = 1278
#Bound   0.64s best:2944  next:[1278,2942] no_lp
#Bound   0.65s best:2944  next:[1352,2942] max_lp
#7       0.65s best:2876  next:[1352,2875] no_lp fixed_bools:1/950
Solution 6, time = 0.663560 s, objective = 2876, best_bound = 1352
#Bound   0.66s best:2876  next:[1352,2874] no_lp
#8       0.67s best:2648  next:[1352,2647] no_lp fixed_bools:1/954
Solution 7, time = 0.679179 s, objective = 2648, best_bound = 1352
#Bound   0.68s best:2648  next:[1352,2646] no_lp
#9       0.70s best:2420  next:[1352,2419] no_lp fixed_bools:1/957
Solution 8, time = 0.710431 s, objective = 2420, best_bound = 1352
#Bound   0.71s best:2420  next:[1352,2418] no_lp
#10      0.72s best:2396  next:[1352,2395] no_lp fixed_bools:1/958
Solution 9, time = 0.726056 s, objective = 2396, best_bound = 1352
#Bound   0.73s best:2396  next:[1352,2394] no_lp
#Bound   0.91s best:2396  next:[1356,2394] pseudo_costs
#11      0.94s best:2260  next:[1356,2259] no_lp fixed_bools:1/960
Solution 10, time = 0.960437 s, objective = 2260, best_bound = 1356
#Bound   0.96s best:2260  next:[1399,2259] max_lp
#Bound   0.97s best:2260  next:[1399,2258] no_lp
#12      0.98s best:2192  next:[1399,2191] no_lp fixed_bools:1/960
Solution 11, time = 0.991691 s, objective = 2192, best_bound = 1399
#Bound   0.99s best:2192  next:[1399,2190] no_lp
#Bound   1.46s best:2192  next:[1450,2190] reduced_costs
#Bound   1.87s best:2192  next:[1461,2190] reduced_costs
#13      2.18s best:2168  next:[1461,2167] default_lp fixed_bools:1/931
Solution 12, time = 2.183427 s, objective = 2168, best_bound = 1461
#Bound   2.19s best:2168  next:[1461,2166] no_lp
#Bound   2.24s best:2168  next:[1473,2166] pseudo_costs
#Bound   2.50s best:2168  next:[1479,2166] pseudo_costs
#14      2.74s best:2032  next:[1479,2031] no_lp fixed_bools:2/968
Solution 13, time = 2.745947 s, objective = 2032, best_bound = 1479
#Bound   2.75s best:2032  next:[1479,2030] no_lp
#Bound   2.83s best:2032  next:[1484,2030] reduced_costs
#Bound   3.23s best:2032  next:[1491,2030] pseudo_costs
#Bound   3.50s best:2032  next:[1493,2030] pseudo_costs
#Bound   3.73s best:2032  next:[1494,2030] pseudo_costs
#Bound   4.89s best:2032  next:[1498,2030] pseudo_costs
#Bound   5.36s best:2032  next:[1500,2030] pseudo_costs
#Bound   8.63s best:2032  next:[1503,2030] pseudo_costs
#Bound   8.85s best:2032  next:[1508,2030] pseudo_costs
#Bound   9.93s best:2032  next:[1514,2030] pseudo_costs
#Bound  10.18s best:2032  next:[1518,2030] pseudo_costs
#Bound  10.80s best:2032  next:[1523,2030] pseudo_costs
#Bound  11.05s best:2032  next:[1527,2030] pseudo_costs
#Bound  11.25s best:2032  next:[1531,2030] pseudo_costs
#Bound  12.14s best:2032  next:[1532,2030] pseudo_costs

......
......
......

#Bound  26.09s best:2032  next:[1647,2030] pseudo_costs
#Bound  26.60s best:2032  next:[1719,2030] pseudo_costs
#Bound  26.76s best:2032  next:[1720,2030] pseudo_costs
#Bound  26.94s best:2032  next:[1724,2030] pseudo_costs
#Done   26.94s pseudo_costs
#Done   26.96s default_lp


Solution

  • You should read this thread.

    In particular, the callback code from stradivari96@

    from threading import Timer
    
    from ortools.sat.python import cp_model
    
    
    class ObjectiveEarlyStopping(cp_model.CpSolverSolutionCallback):
        def __init__(self, timer_limit: int):
            super(ObjectiveEarlyStopping, self).__init__()
            self._timer_limit = timer_limit
            self._timer = None
            self._reset_timer()
    
        def on_solution_callback(self):
            self._reset_timer()
    
        def _reset_timer(self):
            if self._timer:
                self._timer.cancel()
            self._timer = Timer(self._timer_limit, self.StopSearch)
            self._timer.start()
    
        def StopSearch(self):
            print(f"{self._timer_limit} seconds without improvement")
            super().StopSearch()