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 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. */ List candidates; /** - * Indicate which variables are selected in a fragment + * BitSet indicating which variables are currently in the fragment (to be frozen). + * A bit set to 1 means the variable is in the fragment and will be frozen. */ protected BitSet fragment; /** - * Reference to the model + * Reference to the model containing the variables and constraints */ protected Model mModel; /** - * Create a propagation-guided neighbor for LNS + * Constructs a Propagation Guided LNS neighbor. + *

+ * 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 listSize 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. + *

+ * The method stops when either: + *

* - * @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. + *

+ * 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 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. */ List candidates; /** - * Indicate which variables are selected in a fragment + * BitSet indicating which variables are currently in the fragment (frozen). + * A bit set to 1 means the variable is frozen to its solution value. */ protected BitSet fragment; /** - * Reference to the model + * Reference to the model containing the variables and constraints */ protected Model mModel; /** - * Create a reverse adaptive neighbor for LNS based on PGLNS, which selects variables to not be part of a fragment - * @param vars variables to consider - * @param desiredSize desired size of the fragment - * @param listSize number of modified variable to store while propagating - * @param seed for randomness + * Constructs a Reverse Propagation Guided LNS neighbor. + *

+ * 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 listSize 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. + *

+ * 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: + *

+ * + * @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);