Creating Your Own Universal Callback

In this page, we will show you how to create your own universal callback. Universal callbacks are callbacks which are not specific to a particular solver. In that sense, they are generic and can be used with any solver.

Note that there are also solver-specific callbacks, which are specific to a particular solver. If you are looking for this, you can refer to this page.

Creating your own callback can useful if you want to create a callback that is not included in the default set of callbacks implemented in idol. If you are looking for quick and easy-to-use callbacks for separating user cuts or lazy constraints, you can refer to this page.

Basics

Creating your callback is done by creating a sub-class of the Callback class and overriding the Callack::operator() method. It is this method that will be called by the solver at each iteration.

Note, however, that callbacks cannot be given “as-is” to an optimizer but must be passed through a CallbackFactory. A callback factory is a class whose role is to create a new callback object whenever it is needed. Every callback factories must be a child (or little child) of the CallbackFactory class.

The following code shows how to create a callback factory and a callback that prints the value of the primal solution at each iteration.

class MyCallback : public CallbackFactory {
public:

    class Strategy { // Real callback implementation
    protected:
        void operator()(CallbackEvent t_event) {

               if (t_event != IncumbentSolution) {
                    return;
               }

               std::cout << primal_solution() << std::endl;
        }
    }

    Callback* operator()() { // Creates a new callback object
        return new Strategy();
    }

    CallbackFactory* clone() const { // Creates a copy of the callback factory
        return new MyCallback(*this);
    }

}

As you can see, the callback factory has two important methods: operator() and clone(). The operator() method is used to create a new callback object, while the clone() method is used to create a copy of the callback factory.

The nested class Strategy is the actual callback implementation. It is a sub-class of the Callback class and overrides the operator() method. In this example, the callback prints the value of the primal solution whenever the event triggering the callback is IncumbentSolution. In other words, this callback will print out all incumbent solutions found by the solver.

As for the UserCutCallback and LazyConstraintCallback classes, our new callback can be added to an optimizer as follows.

model.use(
    Gurobi().add_callback(MyCallback())
);

model.optimize();

A Simple Example: Knapsack Cover Cuts

Hint

This section is dedicated to the “advanced topic” of knapsack cover inequalities. Rudimentary notions on Knapsack problems and Cover inequalities are recommended.

In this example, we will show how to create a callback that separates knapsack cover cuts. A knapsack cover cut is a valid inequality for the knapsack problem. It is defined as follows:

\[\sum_{i \in C} x_i \leq |C| - 1\]

where \(C\) defines a cover of the knapsack, i.e., a set of items such that the sum of their weights is greater than the capacity of the knapsack.

Given a solution \(\hat x\) to the continuous relaxation of the knapsack problem, we can check whether it violates a cover inequality. This is done by solving the following separation problem.

\[\begin{split}\begin{align} \max_{z} \ & (1 - \hat x)^\top z & \ge 1 \\ \text{s.t.} \ & w^\top z \ge W + 1, \\ & z\in\{0,1\}^n. \end{align}\end{split}\]

A cover inequality is violated if and only if the optimal objective value of this problem is strictly less than 1. In such a case, a new cut should be added.

We will write a callback that separates knapsack cover cuts.

To this end, we first create our knapsack problem model. This is done as follows.

Env env;
Model knapsack(env, Maximize);

const auto x = knapsack.add_vars(Dim<1>(n), 0, 1, Binary, "x");

knapsack.add_ctr(idol_Sum(i, Range(n_items), w[i] * x[i]) <= W);
knapsack.set_obj_expr(idol_Sum(i, Range(n_items), p[i] * x[i]));

Then, we create our callback factory. It is this factory that will be used to create a new callback object when needed. Since we need to pass some parameters to the callback, we will use the constructor of the callback factory to pass these parameters. This is done as follows.

class KnapsackCover : public CallbackFactory {
    const std::vector<Var> m_x;
    const std::vector<double> m_weights;
    const std::vector<double> m_profits;
    const double m_capacity;
public:
    KnapsackCover(const std::vector<Var>& t_x,
                  const std::vector<double>& t_weights,
                  const std::vector<double>& t_profits,
                  double t_capacity)
                    : m_x(t_x), m_weights(t_weights), m_profits(t_profits), m_capacity(t_capacity) {}

    class Strategy;

    Callback* operator()() { // Creates a new callback object
        return new Strategy(m_x, m_weights, m_profits, m_capacity);
    }

    CallbackFactory* clone() const { // Creates a copy of the callback factory
        return new MyCallback(*this);
    }

}

The real implementation of the callback is done in the nested class Strategy. This class is a sub-class of the Callback class and is defined as follows.

class KnapsackCover::Strategy { // Real callback implementation
    const std::vector<Var> m_x;
    const std::vector<double> m_weights;
    const std::vector<double> m_profits;
    const double m_capacity;
protected:
    Strategy(const std::vector<Var>& t_x,
             const std::vector<double>& t_weights,
             const std::vector<double>& t_profits,
             double t_capacity)
                : m_x(t_x), m_weights(t_weights), m_profits(t_profits), m_capacity(t_capacity) {}

    void operator()(CallbackEvent t_event) {

           if (t_event != InvalidSolution) {
                return;
           }

           auto& env = parent().env();
           const auto fractional_point = primal_solution();

            Model separation(env, Maximize);

            const auto z = separation.add_vars(Dim<1>(m_x.size()), 0, 1, Binary, "z");
            separation.add_ctr(idol_Sum(i, Range(m_x.size()), m_weights[i] * z[i]) >= m_capacity + 1);
            separation.set_obj_expr(idol_Sum(i, Range(m_x.size()), (1 - fractional_point[i]) * z[i]));

            separation.use(Gurobi());

            separation.optimize();

            if (separation.get_best_obj() < 1) {
                return;
            }

            const auto cut = idol_Sum(i, Range(m_x.size()), separation.get_var_primal(z[i]) * (1 - x[i])) >= 1;

            add_user_cut(cut);

    }
}

Finally, we can add our callback to the optimizer as follows.

knapsack.use(
    Gurobi::Continuous().add_callback(KnapsackCover(x, w, p, W))
);

knapsack.optimize();