IBM ILOG Solver User's Manual > Extending the Library > Writing a Constraint: Allocating Frequencies > Advanced issues: Propagating constraints after modifying variables

Suppose that you have a constraint C involving the variables x, y, z, and t. Suppose that x is modified, then C is propagated. Now if the modification of x leads to a modification of y, z, and t, these variables are propagated and each of those variables will propagate C again, even if these variables have not been modified by constraints other than C.

Imagine that you want to avoid these repeated propagations because you happen to know that the propagate member function of C is costly. Also imagine that you want to propagate C once for all the modifications of the variables involved in C but not for the modifications of each variable individually.

There is a mechanism in Solver that meets these aims. It is possible to have constraints that are not propagated within demons.

Such a constraint is not propagated after each modification of each variable; rather, it is propagated after all its variables have been propagated.

In fact, in Solver there are two mechanisms for propagating the modifications of variables.

If, after the termination of a propagate member function, a variable has been modified, you immediately return to the first level. This means that the process at the first level has greatest priority.

In the second level, a global constraint (say, ct) is pushed by a call to the push member function, like this ct->push();.

This member function says to Solver that ct is a global constraint and must be propagated by the second level mechanism. This member function must be called within a demon.

Consider the following example simplified for illustration: you would like to define the constraint x + y = s without considering modifications of s.

The code of that constraint could be something like this:

class MySum : public IlcConstraintI {
private:
IlcIntVar _x;
IlcIntVar _y;
IlcIntVar _s;
public:
MySum(IloSolver solver, IlcIntVar x, IlcIntVar y, IlcIntVar s);
void post();
void propagate();
};

MySum::MySum(IloSolver solver, IlcIntVar x, IlcIntVar y, IlcIntVar s):
IlcConstraintI(solver), _x(x), _y(y), _s(s){}

void MySum::propagate(){
        IlcInt m;
        // we compute the min of s
         m=_x.getMin() + _y.getMin();
         _s.setMin(m);
         // we compute the max of s
         m=_x.getMax() + _y.getMax();
         _s.setMax(m);
}

Now you add a demon for each variable x and y, and that demon is triggered when the boundary of x or y is modified (that is, when a range event occurs). Let's say this demon is called MySumDemon, and it takes the global constraint as a parameter. Here is the code for its post member function:

void MySum::post(){
        _x.whenRange(MySumDemon(getSolver(),this));
        _y.whenRange(MySumDemon(getSolver(),this));
}

Now here is the code of the demon. Instead of calling a particular function of ct that you define, you call the push member function, like this:

ILCCTPUSHDEMON(MySumDemon, MySum){
ct->push();
}

How does this tactic differ from the conventional propagation mechanism? In this case, the demon is called, but the propagate member function of MySum is not called immediately. Rather, ct is pushed. When there are no more demons to trigger, the constraints that have been pushed are called. Furthermore, in this way, a constraint is propagated only once even if you push it several times.

If you want to refine the propagation mechanism, for example, if you want to treat the variables that have been modified in a special way, you have to manage that treatment by using internal data structures within the constraint. For example, suppose that if x is modified, you want to call your own function propagateX() and if y has been modified, you want to call your own propagateY(). To do so, you add reversible Boolean data members to MySum, like this:

IlcRevBool _xIsModified
IlcRevBool _yIsModified

Now you define special demons for x and for y, like this:

MySum::demonForX(){
  memorizeThatYisModified();
  push();
}

MySum::demonForY(){
  memorizeThatXIsModified();
  push();
}

ILCCTDEMON0(MySymDemonForX, MySum, demonForX);
ILCCTDEMON0(MySumDemonForY, MySum, demonForY);

void MySum::memorizeThatXisModified(){
_xIsModified=IlcTrue;
}

Now the propagate member function looks like this:

void MySum::propagate(){
        if (_xIsModified){
                propagateX();
              _xIsModified=IlcFalse;
        }
        if (_yIsModified){
                propagateY();
                _yIsModified=IlcFalse;
        }
}

Thus you have created a global constraint that is propagated only once and propagated only after the propagation of variables in that constraint.