diff --git a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java index 34a5d825a6..ce8b31a84f 100644 --- a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java +++ b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java @@ -17,10 +17,22 @@ import java.util.stream.IntStream; /** - * A Propagation Guided LNS + * A Propagation Guided Large Neighborhood Search (LNS) neighbor. *
* Based on "Propagation Guided Large Neighborhood Search", Perron et al. CP2004.
- *
+ *
+ * This implementation selects variables to be part of the fragment (to be frozen) based on + * the impact of constraint propagation. The algorithm maintains a fragment of variables + * that are frozen to their current values. It iteratively selects variables to add to the + * fragment, prioritizing those that cause the most domain reduction when frozen. + *
+ * Variables that cause significant domain reduction in other variables through propagation + * are considered most influential and are prioritized for inclusion in the fragment. + * This creates a dynamic neighborhood that adapts based on constraint propagation effects. + *
+ * This strategy is particularly effective when the constraint propagation provides
+ * strong guidance on which variables are most influential in reducing the search space.
+ * For a reverse approach, see {@link ReversePropagationGuidedNeighborhood}.
*
* @author Charles Prud'homme
* @since 08/04/13
@@ -29,61 +41,79 @@ public class PropagationGuidedNeighborhood extends IntNeighbor {
/**
- * Number of variables
+ * Number of variables in the neighborhood
*/
protected final int n;
/**
- * Domain size of each variable in {@link #variables}
+ * Current domain size of each variable in {@link #variables}
*/
protected int[] curDoms;
/**
- * Domain size of each variable in {@link #variables} before propagation
+ * Domain size of each variable in {@link #variables} before the current propagation step.
+ * Used to compute the domain reduction caused by freezing a variable.
*/
protected int[] befDoms;
/**
- * Store the modified variables
+ * Stores the domain reduction (in absolute values) for each variable,
+ * used to rank variables by their impact on propagation
*/
protected int[] all;
/**
- * For randomness
+ * Random number generator for random variable selection
*/
protected Random rd;
/**
- * Intial size of the fragment
+ * Desired size of the fragment (target logarithmic sum of domain sizes)
*/
final double desiredSize;
/**
- * Current size of the fragment
+ * Current size of the fragment.
+ * This is dynamically adjusted by {@link #restrictLess()} to increase the neighborhood size
+ * over time (size *= 1.01 each call).
*/
double size;
/**
- * Number of variables modified through propagation to consider while computing the neighbor
+ * Maximum number of candidate variables to store and consider.
+ * Only the top
+ * This neighbor selects variables to be part of the fragment (to be frozen) based on
+ * the impact of constraint propagation. Variables that cause the most domain reduction
+ * when frozen are prioritized.
*
- * @param vars set of variables to consider
- * @param desiredSize desired size of the fragment
- * @param listSize number of modified variable to store while propagating
- * @param seed for randomness
+ * @param vars the integer variables to consider for the neighborhood
+ * @param desiredSize the desired size of the fragment (logarithmic sum of domain sizes).
+ * Note: this is a double value representing a target sum, not a count of variables.
+ * @param listSize the number of modified variables to store and consider while propagating.
+ * Variables are ranked by their impact (domain reduction caused) and only the
+ * top
+ * The method stops when either:
+ *
+ * This implementation works in reverse compared to {@link PropagationGuidedNeighborhood}:
+ * instead of selecting variables to be part of the fragment (to be frozen), it selects
+ * variables to NOT be part of the fragment (to be relaxed). The approach is based on
+ * "Propagation Guided Large Neighborhood Search", Perron et al. CP2004.
+ *
+ * The algorithm maintains a fragment of variables that are frozen to their current values.
+ * It iteratively selects variables to remove from the fragment (unfreeze) based on the
+ * impact of propagation. Variables that cause the most domain reduction when frozen are
+ * prioritized for removal, creating a dynamic neighborhood that adapts based on constraint
+ * propagation effects.
+ *
+ * This strategy can be particularly effective when the constraint propagation provides
+ * strong guidance on which variables are most influential in reducing the search space.
*
* @author Charles Prud'homme
* @since 08/04/13
*/
-public class ReversePropagationGuidedNeighborhood extends IntNeighbor{
+public class ReversePropagationGuidedNeighborhood extends IntNeighbor {
/**
- * Number of variables
+ * Number of variables in the neighborhood
*/
protected final int n;
/**
- * Domain size of each variable in {@link #variables}
+ * Initial domain size of each variable in {@link #variables},
+ * recorded during initialization
*/
protected int[] domSiz;
/**
- * Store the modified variables
+ * Stores the domain reduction percentage for each variable,
+ * used to rank variables by their impact on propagation
*/
protected int[] all;
/**
- * For randomness
+ * Random number generator for random variable selection
*/
protected Random rd;
/**
- * Intial size of the fragment
+ * Desired size of the fragment (target logarithmic sum of domain sizes)
*/
final double desiredSize;
/**
- * Goal size of the fragment
+ * Current target size of the fragment (adjusted by epsilon)
*/
double size;
/**
- * Number of variables modified through propagation to consider while computing the neighbor
+ * Maximum number of candidate variables to store and consider.
+ * Only the top
+ * This neighbor selects variables to NOT be part of the fragment (i.e., to relax/freeze).
+ * The selection is guided by the impact of constraint propagation: variables that cause
+ * the most domain reduction when frozen are prioritized.
+ *
+ * @param vars the integer variables to consider for the neighborhood
+ * @param desiredSize the desired size of the fragment (number of variables to freeze)
+ * @param listSize the number of modified variables to store and consider while propagating.
+ * Variables are ranked by their impact (domain reduction caused) and only the
+ * top
+ * The epsilon parameter is adaptively adjusted based on the actual log-sum of domain
+ * sizes encountered during the process, allowing the neighborhood size to adapt over time.
+ *
+ * @throws ContradictionException if fixing variables leads to a contradiction
+ */
@Override
public void fixSomeVariables() throws ContradictionException {
logSum = 0;
@@ -104,12 +144,27 @@ public void fixSomeVariables() throws ContradictionException {
try {
update();
epsilon = (.95 * epsilon) + (.05 * (logSum / size));
- }catch (ContradictionException ce){
+ } catch (ContradictionException ce) {
epsilon = (.95 * epsilon) + (.05 / size);
throw ce;
}
}
+ /**
+ * Updates the fragment by iteratively selecting and removing variables.
+ * For each selected variable, it temporarily freezes the variable to its solution value,
+ * propagates constraints, and measures the impact on other variables' domains.
+ * Variables that cause significant domain reduction in others are prioritized for removal
+ * from the fragment in subsequent iterations.
+ *
+ * The method stops when either:
+ * listSize variables with the highest domain reduction impact
+ * are kept as candidates for the next selection.
*/
int listSize;
/**
- * Logarithmic cardinality of domains
+ * Current logarithmic sum of domain sizes of variables in the fragment.
+ * Used to track progress toward the desired fragment size.
+ * The loop in {@link #update()} continues while logSum > size.
*/
double logSum = 0.;
/**
- * Store the variable elligible for propagation
+ * List of candidate variable indices eligible for selection.
+ * Contains variables from the fragment that caused domain reduction when frozen,
+ * sorted by their impact (highest first) and limited to {@link #listSize} entries.
*/
ListlistSize are kept as candidates for the next selection.
+ * @param seed the seed for the random number generator used when no candidates are available
*/
public PropagationGuidedNeighborhood(IntVar[] vars, double desiredSize, int listSize, long seed) {
super(vars);
@@ -97,6 +127,14 @@ public PropagationGuidedNeighborhood(IntVar[] vars, double desiredSize, int list
this.fragment = new BitSet(n);
}
+ /**
+ * Creates a fragment by freezing variables based on propagation guidance.
+ * Initially computes the logarithmic sum of all variable domain sizes and copies
+ * current domain sizes to {@link #befDoms}. All variables start in the fragment.
+ * Then calls {@link #update()} to iteratively select and freeze variables.
+ *
+ * @throws ContradictionException if the fragment is trivially infeasible
+ */
@Override
public void fixSomeVariables() throws ContradictionException {
logSum = Arrays.stream(variables).mapToDouble(v -> MathUtils.log2(v.getDomainSize())).sum();
@@ -106,9 +144,19 @@ public void fixSomeVariables() throws ContradictionException {
}
/**
- * Create the fragment
+ * Creates the fragment by iteratively selecting and freezing variables.
+ * For each selected variable, it freezes the variable to its solution value,
+ * propagates constraints, and measures the impact on other variables' domains.
+ * Variables that cause significant domain reduction in others are prioritized for
+ * inclusion in the fragment.
+ *
+ *
*
- * @throws ContradictionException if the fragment is trivially infeasible
+ * @throws ContradictionException if propagating the freezing of a variable leads to a contradiction
*/
protected void update() throws ContradictionException {
while (logSum > size && fragment.cardinality() > 0) {
@@ -148,7 +196,12 @@ protected void update() throws ContradictionException {
}
/**
- * @return a variable id in {@link #variables} to be part of the fragment
+ * Selects the next variable to process from the fragment.
+ * If there are candidate variables (those that caused significant domain reduction when
+ * frozen), it prioritizes them (selecting from the head of the list).
+ * Otherwise, it selects a variable randomly from the remaining variables in the fragment.
+ *
+ * @return the index of the selected variable in {@link #variables}
*/
int selectVariable() {
int id;
@@ -163,23 +216,44 @@ int selectVariable() {
return id;
}
+ /**
+ * Loads the neighborhood state from a solution.
+ * Resets the current size to the desired size.
+ *
+ * @param solution the solution to load from
+ */
@Override
public void loadFromSolution(Solution solution) {
super.loadFromSolution(solution);
size = desiredSize;
}
+ /**
+ * Records the current solution.
+ * Resets the current size to the desired size after recording.
+ */
@Override
public void recordSolution() {
super.recordSolution();
size = desiredSize;
}
+ /**
+ * Restricts the neighborhood less by increasing the fragment size.
+ * Multiplies the current size by 1.01, allowing the neighborhood to grow
+ * over time and explore larger fragments.
+ */
@Override
public void restrictLess() {
size *= 1.01;
}
+ /**
+ * Initializes the neighborhood by recording the initial domain sizes of all variables.
+ * This is called once at the beginning of the search to establish baseline domain sizes
+ * in {@link #curDoms} and {@link #befDoms} that are used to measure the impact of
+ * freezing variables during the neighborhood exploration.
+ */
@Override
public void init() {
this.curDoms = new int[n];
diff --git a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java
index 440f1a9ee3..6f6661229f 100644
--- a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java
+++ b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java
@@ -17,71 +17,100 @@
import java.util.stream.IntStream;
/**
- * A Propagation Guided LNS
- *
- * Based on "Propagation Guided Large Neighborhood Search", Perron et al. CP2004.
- *
+ * A Reverse Propagation Guided Large Neighborhood Search (LNS) neighbor.
+ * listSize variables with the highest domain reduction impact
+ * are kept as candidates for the next selection.
*/
int listSize;
/**
- * Logarithmic cardinality of domains
+ * Current logarithmic sum of domain sizes of frozen variables.
+ * Used to track progress toward the desired fragment size.
*/
double logSum = 0.;
/**
- * Restriction parameter
+ * Adaptive restriction parameter that adjusts the fragment size dynamically.
+ * It is updated after each call to {@link #fixSomeVariables()} based on the
+ * actual log-sum achieved, allowing the algorithm to adapt to the problem structure.
+ * A value greater than 1.0 increases the fragment size, while a value less than 1.0 decreases it.
*/
private double epsilon = 1.;
/**
- * Store the variable elligible for propagation
+ * List of candidate variable indices eligible for selection.
+ * Contains variables from the fragment that caused domain reduction when frozen,
+ * sorted by their impact (highest first) and limited to {@link #listSize} entries.
*/
ListlistSize are kept as candidates for the next selection.
+ * @param seed the seed for the random number generator used when no candidates are available
*/
public ReversePropagationGuidedNeighborhood(IntVar[] vars, int desiredSize, int listSize, long seed) {
super(vars);
@@ -96,6 +125,17 @@ public ReversePropagationGuidedNeighborhood(IntVar[] vars, int desiredSize, int
this.fragment = new BitSet(n);
}
+ /**
+ * Creates a fragment by freezing variables based on reverse propagation guidance.
+ * Initially, all variables are considered frozen (part of the fragment).
+ * The method iteratively removes variables from the fragment until the desired size
+ * is reached or a contradiction is detected.
+ *
+ *
+ *
+ * @throws ContradictionException if propagating the freezing of a variable leads to a contradiction
+ */
protected void update() throws ContradictionException {
while (logSum < size && fragment.cardinality() > 0) {
// 1. pick a variable
@@ -121,7 +176,10 @@ protected void update() throws ContradictionException {
mModel.getSolver().pushTrail();
variables[id].instantiateTo(values[id], Cause.Null);
- mModel.getSolver().propagate();
+ try {
+ mModel.getSolver().propagate();
+ } catch (ContradictionException ignored) {
+ }
fragment.clear(id);
for (int i = 0; i < n; i++) {
@@ -156,7 +214,12 @@ protected void update() throws ContradictionException {
}
/**
- * @return a variable id in {@link #variables} to be part of the fragment
+ * Selects the next variable to process from the fragment.
+ * If there are candidate variables (those that caused significant domain reduction when
+ * frozen), it prioritizes them. Otherwise, it selects a variable randomly from the
+ * remaining variables in the fragment.
+ *
+ * @return the index of the selected variable in {@link #variables}
*/
int selectVariable() {
int id;
@@ -171,6 +234,11 @@ int selectVariable() {
return id;
}
+ /**
+ * Initializes the neighborhood by recording the initial domain sizes of all variables.
+ * This is called once at the beginning of the search to establish baseline domain sizes
+ * that are used to measure the impact of freezing variables during the neighborhood exploration.
+ */
@Override
public void init() {
this.domSiz = new int[n];
diff --git a/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java b/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java
index e12a50668d..604eae8329 100644
--- a/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java
+++ b/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java
@@ -30,6 +30,8 @@
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import java.util.Arrays;
+
import static java.lang.Math.ceil;
import static org.chocosolver.solver.search.strategy.Search.domOverWDegSearch;
import static org.chocosolver.solver.search.strategy.Search.lastConflict;
@@ -62,7 +64,8 @@ private void knapsack20(final int lns) {
Solver r = model.getSolver();
r.setSearch(lastConflict(domOverWDegSearch(objects)));
- r.limitTime(900);
+// r.limitTime(900);
+ r.limitNode(3000);
switch (lns) {
case 0:
break;
@@ -103,7 +106,7 @@ private void knapsack20(final int lns) {
// So, the least we can do is to check that a solution is found
Assert.assertTrue(r.getSolutionCount() > 0, "No solution found with LNS=" + lns);
// We can also check that the weight is not that bad
- Assert.assertTrue(bp >= 7900, "Power is too low with LNS=" + lns);
+ Assert.assertTrue(bp >= 7900, "Power is too low "+bp+" with LNS=" + lns);
Assert.assertTrue(bw >= 1090, "Weight is too low with LNS=" + lns);
}
@@ -301,7 +304,7 @@ public void testPN1() {
}
int[] coeffs = new int[nodes];
- for (int i = 0; i < nodes; i++) coeffs[i] = 1;
+ Arrays.fill(coeffs, 1);
model.scalar(costmaxs, coeffs, "=", optVar).post();
model.setObjective(Model.MINIMIZE, optVar);