Propagation Engine¶
The propagation engine is the heart of any CP solver.
In MaxiCP it is implemented in the MaxiCP class and follows a priority-based
fixed-point algorithm.
Source: MaxiCP.java
Priority-Based Scheduling¶
Constraints implement the CPConstraint interface, which exposes a propagate()
method and a priority() method.
When a variable domain changes, every constraint subscribed to that event is scheduled
for propagation.
The engine drains a priority queue so that cheap constraints (bounds, equality) run before
expensive global constraints (AllDifferent, Cumulative).
public void fixPoint() {
notifyFixPoint();
while (!propagationQueue.isEmpty()) {
CPConstraint c = propagationQueue.poll();
c.setScheduled(false);
if (c.isActive()) c.propagate();
}
}
public void schedule(CPConstraint c) {
if (c.isActive() && !c.isScheduled()) {
c.setScheduled(true);
propagationQueue.add(c, c.priority());
}
}
If propagate() throws InconsistencyException the queue is cleared and the
exception propagates to the search layer, which handles backtracking.
Event-Driven Constraint Activation¶
Constraints register to specific domain events. Only the relevant constraints are re-scheduled when a variable changes.
Integer variables (``CPIntVar``)
propagateOnFix— variable assigned a single value.propagateOnBoundChange— minimum or maximum changes.propagateOnDomainChange— any value removed.
Interval variables (``CPIntervalVar``)
propagateOnChange— any attribute (start, end, length, presence) changes.propagateOnFix— interval fully fixed.
Sequence variables (``CPSeqVar``)
propagateOnInsert— a node is inserted into the partial sequence.propagateOnExclude— a node is excluded.propagateOnRequire— a node becomes required.
Registering a constraint to an event is a reversible operation and is automatically undone on backtrack.
Posting Constraints¶
cp.post(constraint) invokes post(), which typically:
Registers the constraint to the relevant variable events.
Calls
propagate()for an initial domain reduction.
By default, a full fixed-point is computed immediately.
Use cp.post(constraint, false) to defer it and trigger it later via cp.fixPoint().
Because constraint registration is reversible, branching decisions can simply be implemented by posting constraints that define each alternative branch.
Implementing a Custom Constraint¶
Extend AbstractCPConstraint and implement post() and propagate().
Source: LessOrEqual.java
public class LessOrEqual extends AbstractCPConstraint {
private final CPIntVar x, y;
public LessOrEqual(CPIntVar x, CPIntVar y) {
super(x.getSolver());
this.x = x; this.y = y;
}
@Override
public void post() {
x.propagateOnBoundChange(this);
y.propagateOnBoundChange(this);
propagate();
}
@Override
public void propagate() {
x.removeAbove(y.max());
y.removeBelow(x.min());
}
}
removeAbove / removeBelow throw InconsistencyException when the domain
becomes empty, triggering backtracking.
Delta-Based Incremental Propagation¶
For higher performance, a constraint can subscribe to delta events and react only to the values removed since its last execution, instead of re-scanning full domains.
The Element constraint (z = t[y]) below uses this approach.
y.delta(this) returns a DeltaCPIntVar; calling yDelta.fillArray(buf) fills
buf with the values removed from y since the last propagation call
and returns their count.
Source: Element1D.java
public class Element extends AbstractCPConstraint {
private final int[] t;
private final CPIntVar y, z;
private DeltaCPIntVar yDelta, zDelta;
private int[] yValues, zValues;
private int offZ;
private StateInt[] supportCounter;
public Element(int[] array, CPIntVar y, CPIntVar z) {
super(y.getSolver()); this.t = array; this.y = y; this.z = z;
}
@Override
public void post() {
y.removeBelow(0); y.removeAbove(t.length - 1);
y.propagateOnDomainChange(this);
yDelta = y.delta(this);
yValues = new int[y.size()];
z.removeBelow(Arrays.stream(t).min().getAsInt());
z.removeAbove(Arrays.stream(t).max().getAsInt());
z.propagateOnDomainChange(this);
zDelta = z.delta(this);
zValues = new int[z.size()];
supportCounter = new StateInt[z.size()];
offZ = z.min();
for (int i = 0; i < z.size(); i++)
supportCounter[i] = getSolver().getStateManager().makeStateInt(0);
for (int i = 0; i < t.length; i++)
if (y.contains(i)) supportCounter[t[i] - offZ].increment();
propagate();
}
@Override
public void propagate() {
if (zDelta.size() > 0) {
int sz = zDelta.fillArray(zValues);
for (int i = 0; i < sz; i++) {
int v = zValues[i];
int sy = y.fillArray(yValues);
for (int j = 0; j < sy; j++)
if (t[yValues[j]] == v) y.remove(yValues[j]);
}
}
if (yDelta.size() > 0) {
int sy = yDelta.fillArray(yValues);
for (int i = 0; i < sy; i++) {
int v = yValues[i];
if (supportCounter[t[v] - offZ].decrement() == 0)
z.remove(t[v]);
}
}
}
}
supportCounter is an array of StateInt objects whose values are automatically
restored on backtrack, keeping the counts consistent with the current search state.
Global Constraints¶
Source of the constraints package: org.maxicp.cp.engine.constraints
Integer / Boolean constraints
Constraint (source) |
Semantics |
|---|---|
All variables take pairwise distinct values. Uses Régin’s arc-consistent filtering based on maximum bipartite matching [7]. |
|
Forward-checking (bounds-consistent) variant of AllDifferent, cheaper but weaker than DC. |
|
Constrains the number of variables in |
|
The number of distinct values taken by the variables is at least |
|
|
|
Binary knapsack: each item is either taken ( |
|
Upper / lower cardinality bounds on how many variables take each value (forward-checking). |
|
|
|
Weaker form of Circuit allowing sub-tours; used for open routing models. |
|
AllDifferent with an assignment cost matrix; filters both the all-different and the total-cost variable using min-cost matching. |
|
Global Cardinality Constraint (upper bounds only) combined with a cost variable: |
|
Soft GCC: for each value |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
At least one Boolean variable in the array is true: |
|
Reified disjunction: |
|
Reified equality: |
|
Reified inequality: |
|
Positive extensional constraint: the assignment of |
|
Negative extensional constraint (forbidden tuples): the assignment must not match any row in |
|
Short (starred) table constraint with wildcard |
Scheduling constraints (org.maxicp.cp.engine.constraints.scheduling)
Constraint (source) |
Semantics |
|---|---|
No two interval variables in the group execute at the same time. Filtering uses Vilim’s edge-finding and not-first/not-last algorithms (Theta-Lambda tree) [13]. |
|
Bounds-consistent (overload-check + detectable precedences only) variant of NoOverlap; faster but weaker. |
|
Pairwise disjunctive constraint over exactly two interval variables: |
|
Pairwise disjunctive constraint with a sequence-dependent setup time between two tasks. |
|
Decomposition-based cumulative constraint (reference implementation). |
|
EndBeforeStart and temporal ordering family |
Temporal precedence constraints between interval variables. The family covers all combinations of |
|
Sequence-variable constraints (org.maxicp.cp.engine.constraints.seqvar)
Constraint (source) |
Semantics |
|---|---|
Inserts |
|
Removes |
|
Marks |
|
Forbids |
|
Node |
|
|
|
For each consecutive pair |
|
Pickup-and-delivery load constraint on a sequence variable: enforces capacity at every node, pickup-before-delivery ordering, and that both nodes of a request belong to the same sequence [15]. |
|
A given partial sequence |
|
Fixes all non-relaxed nodes to follow their order in a reference solution while leaving relaxed nodes free for re-insertion. Used in LNS. |