Here is an example with a simple callback that works (you can ignore the code that is grayed out):
from threading import Timer
from itertools import permutations
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()
if __name__ == "__main__":
model = cp_model.CpModel()
nodes = range(1, 100)
all_arcs = []
literals = {}
for i in nodes:
literals[0, i] = model.NewBoolVar(f"0 -> {i}")
literals[i, 0] = model.NewBoolVar(f"{i} -> 0")
all_arcs.append([0, i, literals[0, i]])
all_arcs.append([i, 0, literals[i, 0]])
for i, j in permutations(nodes, 2):
literals[i, j] = model.NewBoolVar(f"{i} -> {j}")
all_arcs.append([i, j, literals[i, j]])
model.AddCircuit(all_arcs)
model.Maximize(sum(literals[i, j] * abs(i - j) for i, j in permutations(nodes, 2)))
solver = cp_model.CpSolver()
solver.parameters.cp_model_presolve = False
solver.parameters.num_search_workers = 1
solver.parameters.log_search_progress = True
solver.SolveWithSolutionCallback(model, ObjectiveEarlyStopping(5))