LocalSolver logo
is now
Hexaly logo

We're excited to share that we are moving forward. We're leaving behind the LocalSolver brand and transitioning to our new identity: Hexaly. This represents a leap forward in our mission to enable every organization to make better decisions faster when faced with operational and strategic challenges.


Callbacks and events

LocalSolver allows you to react to specific events during the search by calling your own functions/procedures. It can be used for example to control when to stop the search or to display some specific information during the search.

When adding a callback function to LocalSolver (with the addCallback() function) you can specify to which event your function must react, thanks to the LSCallbackType enumeration:

  • PhaseEnded: your function will be called at the end of each search phase.

  • PhaseStarted: your function will be called at the beginning of each search phase.

  • Display: your function will be called periodically, after a display on the console or in the log file, every timeBetweenDisplays seconds.

  • TimeTicked: your function will be called periodically, every timeBetweenTicks seconds.

  • IterationTicked: your function will be called periodically, every iterationBetweenTicks iterations.

The same callback can be used for different events. The parameters timeBetweenDisplays, timeBetweenTicks and iterationBetweenTicks can be modified in the LSParam object.

When a callback is called, the solver is paused. In that state, you can call all the methods of the API marked as “allowed in state Paused”. For example, you can:

  • Stop the resolution.

  • Retrieve the current solution.

  • Retrieve the statistics of the search.

In this section, we detail how callback functions are introduced in each programming language (LSP, Python, C++, Java, .NET). To illustrate this description we will use a simple example where a callback function allows to stop the search when no improvement of the objective function is found during a period of 5 seconds (the solved problem is a random knapsack).

To add callbacks in an LSP program, you will need to use main mode which will allow you to use the modeler as an ordinary programming language. In this situation, a callback is simply a function taking two parameters: the LocalSolver object that triggers the event and the type of the callback. It is possible to use the same callback method for multiple events or multiple LocalSolver instances.

In the following exampe, we create a dedicated function referring to two global values defined in the main function, lastBestValue and lastBestRunningTime. The callback method uses these two values with the statistics of LocalSolver to decide to stop or to continue the seach. The callback is registered with the method LocalSolver.addCallback():

function callbackExample(ls, cbType) {
    local stats = ls.statistics;
    local obj = ls.model.objectives[0];
    if (obj.value > lastBestValue) {
        lastBestRunningTime = stats.runningTime;
        lastBestValue = obj.value;
    }
    if (stats.runningTime - lastBestRunningTime > 5) {
        println(">>>>>>> No improvement during 5 seconds: resolution is stopped");
        ls.stop();
    } else {
        println(">>>>>> Objective " + obj.value);
    }
}

function main(args) {
    lastBestValue = 0;
    lastBestRunningTime = 0;

    with(ls = localsolver.create()) {
        ls.addCallback("ITERATION_TICKED", callbackExample);
    }
}

Each time this callback will be invoked by LocalSolver (namely every TimeBetweenTicks seconds) it retrieves the statistics of the search and considers the total running time and the current best value of the objective function. If no improvement has been found during 5 consecutive seconds, it calls the stop() function to stop the search. Note that the effect of stop() will be taken into account after the end of the callback.

In Python, a callback is simply a function or a method passed to LocalSolver. A callback takes two parameters: the LocalSolver object that triggers the event and the type of the callback. It is possible to use the same callback method or object for multiple events or multiple LocalSolver instances. The method can be a static function or a method on a class.

In the following example, we create a dedicated class with two fields last_best_value and last_best_running_time. The callback method uses these two fields with the statistics of LocalSolver to decide to stop or to continue the search. The callback is registered with the method LocalSolver.add_callback():

class CallbackExample:
    def __init__(self):
        self.last_best_value = 0
        self.last_best_running_time = 0

    def my_callback(self, ls, cb_type):
        stats = ls.statistics
        obj = ls.model.objectives[0]
        if obj.value > self.last_best_value:
            self.last_best_running_time = stats.running_time
            self.last_best_value = obj.value
        if stats.running_time - self.last_best_running_time > 5:
            print(">>>>>>> No improvement during 5 seconds: resolution is stopped")
            ls.stop()
        else:
            print(">>>>>> Objective %d" % obj.value)

ls = localsolver.LocalSolver()
cb = CallbackExample()
ls.add_callback(localsolver.LSCallbackType.TIME_TICKED, cb.my_callback)

Each time this callback will be invoked by LocalSolver (namely every LSParameter.time_between_ticks seconds) it retrieves the statistics of the search and considers the total running time and the current best value of the objective function. If no improvement has been found during 5 consecutive seconds, it calls the stop() function to stop the search. Note that the effect of stop() will be taken into account after the end of the callback.

In C++, a callback function is passed to LocalSolver as an object extending the localsolver::LSCallback class. This class has a single virtual method localsolver::LSCallback::callback() taking two parameters: the LocalSolver object that triggers the event and the type of the callback. It is possible to use the same callback object for multiple events or multiple LocalSolver instances.

Here we create a small class MyCallback containing two fields lastBestValue and lastBestRunningTime. The callback method uses these two fields with the statistics of LocalSolver to decide to stop or to continue the search. The callback is registered with the method localsolver::LocalSolver::addCallback():

class MyCallback : public LSCallback {
private:
    int lastBestRunningTime;
    lsint lastBestValue;

public:
    MyCallback() {
        lastBestRunningTime = 0;
        lastBestValue = 0;
    }

    void callback(LocalSolver& ls, LSCallbackType type) {
        LSStatistics stats = ls.getStatistics();
        LSExpression obj = ls.getModel().getObjective(0);

        if(obj.getValue() > lastBestValue) {
            lastBestRunningTime = stats.getRunningTime();
            lastBestValue = obj.getValue();
        }

        if(stats.getRunningTime() - lastBestRunningTime > 5) {
            cout << ">>>>>>> No improvement during 5 seconds: resolution is stopped" << endl;
            ls.stop();
        } else {
            cout << ">>>>>> Objective improved by " << (obj.getValue() - lastBestValue) << endl;
        }
    }
};

LocalSolver ls;
MyCallback cb;
ls.addCallback(CT_TimeTicked, &cb);

Each time this callback will be invoked by LocalSolver (namely every timeBetweenTicks seconds) it retrieves the statistics of the search and considers the total running time and the current best value of the objective function. If no improvement has been found during 5 consecutive seconds, it calls the stop() function to stop the search. Note that the effect of stop() will be taken into account after the end of the callback.

In .NET, a callback function is passed to LocalSolver as a delegate method taking two parameters: the LocalSolver object that triggers the event and the type of the callback. It is possible to use the same callback object for multiple events or multiple LocalSolver instances. As any delegate, the callback can be a static method or an instance method.

In the example below, we use an instance method to use two user-defined fields lastBestValue and lastBestRunningTime. The callback is registered with the instruction ls.AddCallback(LSCallbackType.TimeTicked, cb.MyCallback);, while MyCallback is defined as follows:

class MyCallbackClass {
    private int lastBestRunningTime;
    private long lastBestValue;

    public void MyCallback(LocalSolver ls, LSCallbackType type)
    {
        LSStatistics stats = ls.GetStatistics();
        LSExpression obj = ls.GetModel().GetObjective(0);

        if (obj.GetValue() > lastBestValue)
        {
            lastBestRunningTime = stats.GetRunningTime();
            lastBestValue = obj.GetValue();
        }

        if (stats.GetRunningTime() - lastBestRunningTime > 5)
        {
            Console.WriteLine(">>>>>>> No improvement during 5 seconds: resolution is stopped");
            ls.Stop();
        }
        else
        {
            Console.WriteLine(">>>>>> Objective " + (obj.GetValue()));
        }
    }
}

LocalSolver ls = new LocalSolver();
MyCallbackClass cb = new MyCallbackClass();
ls.AddCallback(LSCallbackType.TimeTicked, cb.MyCallback);

Each time this callback will be invoked by LocalSolver (namely every TimeBetweenTicks seconds) it retrieves the statistics of the search and considers the total running time and the current best value of the objective function. If no improvement has been found during 5 consecutive seconds, it calls the Stop() function to stop the search. Note that the effect of Stop() will be taken into account after the end of the callback.

In Java, a callback function is passed to LocalSolver as an object implementing the LSCallback interface. This interface has a single method callback taking two parameters: the LocalSolver object that triggers the event and the type of the callback. It is possible to use the same callback object for multiple events or multiple LocalSolver instances.

The callback class can be conveniently introduced as an anonymous class:

ls.addCallback(LSCallbackType.TimeTicked, new LSCallback() {
    private long lastBestValue = 0;
    private int lastBestRunningTime = 0;

    public void callback(LocalSolver ls, LSCallbackType type) {
        assert(type == LSCallbackType.TimeTicked);

        LSStatistics stats = ls.getStatistics();
        LSExpression obj = ls.getModel().getObjective(0);
        if(obj.getValue() > lastBestValue) {
            lastBestRunningTime = stats.getRunningTime();
            lastBestValue = obj.getValue();
        }

        if(stats.getRunningTime() - lastBestRunningTime > 5) {
            System.out.println(">>>>>>> No improvement during 5 seconds: resolution is stopped");
            ls.stop();
        } else {
            System.out.println(">>>>>> Objective improved by " + (obj.getValue() - lastBestValue));
        }
    }
});

In the above code, an anonymous LSCallback object is introduced, with two numeric fields (lastBestValue and lastBestRunningTime). Each time this callback will be invoked by LocalSolver (namely every timeBetweenTicks seconds) it retrieves the statistics of the search and considers the total running time and the current best value of the objective function. If no improvement has been found during 5 consecutive seconds, it calls the stop() function to stop the search. Note that the effect of stop() will be taken into account after the end of the callback.

Notice that if you are loading your LSP program with the LSPModeler environment, you can use the callbacks from your preferred API (Python, C++, Java, or .Net) without having to necessarily use the main mode for LSP.