Skip to content

Commit ca59667

Browse files
committed
fix fading tabu
1 parent 4dbba25 commit ca59667

4 files changed

Lines changed: 56 additions & 41 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/AbstractTabuAcceptor.java

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -142,21 +142,15 @@ public boolean isAccepted(LocalSearchMoveScope<Solution_> moveScope) {
142142
logIndentation, moveScope.getMove());
143143
return false;
144144
}
145-
// From this point, we are guaranteed to be in a fading tabu.
146-
var acceptChance = calculateFadingTabuAcceptChance(tabuStepCount - workingTabuSize);
147-
// Only call RNG when necessary.
148-
var accepted = Double.compare(acceptChance, 1.0d) >= 0
149-
|| (Double.compare(acceptChance, 0.0d) > 0 && moveScope.getWorkingRandom().nextDouble() < acceptChance);
150-
if (accepted) {
145+
var decision = decideFadingTabuAcceptance(moveScope, tabuStepCount - workingTabuSize);
146+
if (decision.accepted()) {
151147
logger.trace("{} Proposed move ({}) is fading tabu with acceptChance ({}) and is accepted.",
152-
logIndentation,
153-
moveScope.getMove(), acceptChance);
148+
logIndentation, moveScope.getMove(), decision.acceptChance());
154149
} else {
155150
logger.trace("{} Proposed move ({}) is fading tabu with acceptChance ({}) and is not accepted.",
156-
logIndentation,
157-
moveScope.getMove(), acceptChance);
151+
logIndentation, moveScope.getMove(), decision.acceptChance());
158152
}
159-
return accepted;
153+
return decision.accepted();
160154
}
161155

162156
private int locateMaximumTabuStepIndex(LocalSearchMoveScope<Solution_> moveScope) {
@@ -188,12 +182,32 @@ private int locateMaximumTabuStepIndex(LocalSearchMoveScope<Solution_> moveScope
188182

189183
/**
190184
* @param fadingTabuStepCount {@code 0 < fadingTabuStepCount <= fadingTabuSize}
191-
* @return {@code 0.0 < acceptChance < 1.0}
185+
* @return a record with the accept chance, and the decision
192186
*/
193-
protected double calculateFadingTabuAcceptChance(int fadingTabuStepCount) {
194-
// The + 1's are because acceptChance should not be 0.0 or 1.0
195-
// when (fadingTabuStepCount == 0) or (fadingTabuStepCount + 1 == workingFadingTabuSize)
196-
return (workingFadingTabuSize - fadingTabuStepCount) / ((double) (workingFadingTabuSize + 1));
187+
private FadingTabuDecision decideFadingTabuAcceptance(LocalSearchMoveScope<Solution_> moveScope, int fadingTabuStepCount) {
188+
// Invert the chance; the longer the element is in the tabu list, the higher the chance should be.
189+
var numerator = workingFadingTabuSize - fadingTabuStepCount;
190+
if (numerator <= 0) { // The inverted chance would be >= 1.
191+
return FadingTabuDecision.CERTAIN;
192+
}
193+
var denominator = workingFadingTabuSize + 1;
194+
if (numerator >= denominator) { // The inverted chance would be <= 0.
195+
return FadingTabuDecision.IMPOSSIBLE;
196+
}
197+
var acceptChance = 1.0d - (numerator / (double) denominator);
198+
var accepted = Double.compare(moveScope.getWorkingRandom().nextDouble(), acceptChance) < 0;
199+
return new FadingTabuDecision(acceptChance, accepted);
200+
}
201+
202+
/**
203+
* @param acceptChance 0.0 <= acceptChance <= 1
204+
* @param accepted
205+
*/
206+
private record FadingTabuDecision(double acceptChance, boolean accepted) {
207+
208+
private static final FadingTabuDecision CERTAIN = new FadingTabuDecision(1.0d, true);
209+
private static final FadingTabuDecision IMPOSSIBLE = new FadingTabuDecision(0.0d, false);
210+
197211
}
198212

199213
/**

core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/EntityTabuAcceptorTest.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -273,35 +273,35 @@ void fadingTabuSize() {
273273
acceptor.stepEnded(stepScope2);
274274
phaseScope.setLastCompletedStepScope(stepScope2);
275275

276-
// Step 3: fading zone, fadingCount=1, acceptChance=0.6; random=0.3 → accepted
276+
// Step 3: fading zone, fadingCount=1, acceptChance=0.4; random=0.3 → accepted
277277
solverScope.setWorkingRandom(new TestRandom(0.3));
278278
var stepScope3 = new LocalSearchStepScope<>(phaseScope);
279279
assertThat(acceptor.isAccepted(buildMoveScope(stepScope3, e1))).isTrue();
280280
stepScope3.setStep(buildMoveScope(stepScope3, e0).getMove());
281281
acceptor.stepEnded(stepScope3);
282282
phaseScope.setLastCompletedStepScope(stepScope3);
283283

284-
// Step 4: fadingCount=2, acceptChance=0.4; random=0.5 → rejected
284+
// Step 4: fadingCount=2, acceptChance=0.6; random=0.5 → accepted
285285
solverScope.setWorkingRandom(new TestRandom(0.5));
286286
var stepScope4 = new LocalSearchStepScope<>(phaseScope);
287-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope4, e1))).isFalse();
287+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope4, e1))).isTrue();
288288
stepScope4.setStep(buildMoveScope(stepScope4, e0).getMove());
289289
acceptor.stepEnded(stepScope4);
290290
phaseScope.setLastCompletedStepScope(stepScope4);
291291

292-
// Step 5: fadingCount=3, acceptChance=0.2; random=0.1 → accepted
292+
// Step 5: fadingCount=3, acceptChance=0.8; random=0.1 → accepted
293293
solverScope.setWorkingRandom(new TestRandom(0.1));
294294
var stepScope5 = new LocalSearchStepScope<>(phaseScope);
295295
assertThat(acceptor.isAccepted(buildMoveScope(stepScope5, e1))).isTrue();
296296
stepScope5.setStep(buildMoveScope(stepScope5, e0).getMove());
297297
acceptor.stepEnded(stepScope5);
298298
phaseScope.setLastCompletedStepScope(stepScope5);
299299

300-
// Step 6: fadingCount=4, acceptChance=0.0; random consumed but always false
300+
// Step 6: fadingCount=4, acceptChance=1.0; random not consumed and accepted
301301
// adjustTabuList removes e1 (tabuStepCount=6 ≥ totalTabuListSize=6)
302-
solverScope.setWorkingRandom(new TestRandom(0.99));
302+
solverScope.setWorkingRandom(new TestRandom(new double[0]));
303303
var stepScope6 = new LocalSearchStepScope<>(phaseScope);
304-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope6, e1))).isFalse();
304+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope6, e1))).isTrue();
305305
stepScope6.setStep(buildMoveScope(stepScope6, e0).getMove());
306306
acceptor.stepEnded(stepScope6);
307307
phaseScope.setLastCompletedStepScope(stepScope6);

core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/MoveTabuAcceptorTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,35 +155,35 @@ void fadingTabuSize() {
155155
acceptor.stepEnded(stepScope2);
156156
phaseScope.setLastCompletedStepScope(stepScope2);
157157

158-
// Step 3: fadingCount=1, acceptChance=0.6; random=0.3 → accepted
158+
// Step 3: fadingCount=1, acceptChance=0.4; random=0.3 → accepted
159159
solverScope.setWorkingRandom(new TestRandom(0.3));
160160
var stepScope3 = new LocalSearchStepScope<>(phaseScope);
161161
assertThat(acceptor.isAccepted(buildMoveScope(stepScope3, m1))).isTrue();
162162
stepScope3.setStep(m0);
163163
acceptor.stepEnded(stepScope3);
164164
phaseScope.setLastCompletedStepScope(stepScope3);
165165

166-
// Step 4: fadingCount=2, acceptChance=0.4; random=0.5 → rejected
166+
// Step 4: fadingCount=2, acceptChance=0.6; random=0.5 → accepted
167167
solverScope.setWorkingRandom(new TestRandom(0.5));
168168
var stepScope4 = new LocalSearchStepScope<>(phaseScope);
169-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope4, m1))).isFalse();
169+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope4, m1))).isTrue();
170170
stepScope4.setStep(m0);
171171
acceptor.stepEnded(stepScope4);
172172
phaseScope.setLastCompletedStepScope(stepScope4);
173173

174-
// Step 5: fadingCount=3, acceptChance=0.2; random=0.1 → accepted
175-
solverScope.setWorkingRandom(new TestRandom(0.1));
174+
// Step 5: fadingCount=3, acceptChance=0.8; random=0.9 → not accepted
175+
solverScope.setWorkingRandom(new TestRandom(0.9));
176176
var stepScope5 = new LocalSearchStepScope<>(phaseScope);
177-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope5, m1))).isTrue();
177+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope5, m1))).isFalse();
178178
stepScope5.setStep(m0);
179179
acceptor.stepEnded(stepScope5);
180180
phaseScope.setLastCompletedStepScope(stepScope5);
181181

182-
// Step 6: fadingCount=4, acceptChance=0.0; random consumed but always false
182+
// Step 6: fadingCount=4, acceptChance=1.0; random not consumed but always true
183183
// adjustTabuList removes m1 (tabuStepCount=6 ≥ totalTabuListSize=6)
184-
solverScope.setWorkingRandom(new TestRandom(0.99));
184+
solverScope.setWorkingRandom(new TestRandom(new double[0]));
185185
var stepScope6 = new LocalSearchStepScope<>(phaseScope);
186-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope6, m1))).isFalse();
186+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope6, m1))).isTrue();
187187
stepScope6.setStep(m0);
188188
acceptor.stepEnded(stepScope6);
189189
phaseScope.setLastCompletedStepScope(stepScope6);

core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/ValueTabuAcceptorTest.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
1515
import ai.timefold.solver.core.preview.api.move.Move;
1616
import ai.timefold.solver.core.testdomain.TestdataValue;
17+
import ai.timefold.solver.core.testutil.TestRandom;
1718

1819
import org.junit.jupiter.api.Test;
1920

@@ -314,7 +315,7 @@ void fadingTabuSize() {
314315

315316
var solverScope = new SolverScope<>();
316317
solverScope.setInitializedBestScore(SimpleScore.ZERO);
317-
solverScope.setWorkingRandom(new ai.timefold.solver.core.testutil.TestRandom(new double[0]));
318+
solverScope.setWorkingRandom(new TestRandom(new double[0]));
318319
var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0);
319320
acceptor.phaseStarted(phaseScope);
320321

@@ -336,35 +337,35 @@ void fadingTabuSize() {
336337
acceptor.stepEnded(stepScope2);
337338
phaseScope.setLastCompletedStepScope(stepScope2);
338339

339-
solverScope.setWorkingRandom(new ai.timefold.solver.core.testutil.TestRandom(0.3));
340+
solverScope.setWorkingRandom(new TestRandom(0.3));
340341
var stepScope3 = new LocalSearchStepScope<>(phaseScope);
341342
assertThat(acceptor.isAccepted(buildMoveScope(stepScope3, v1))).isTrue();
342343
stepScope3.setStep(buildMoveScope(stepScope3, v0).getMove());
343344
acceptor.stepEnded(stepScope3);
344345
phaseScope.setLastCompletedStepScope(stepScope3);
345346

346-
solverScope.setWorkingRandom(new ai.timefold.solver.core.testutil.TestRandom(0.5));
347+
solverScope.setWorkingRandom(new TestRandom(0.5));
347348
var stepScope4 = new LocalSearchStepScope<>(phaseScope);
348-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope4, v1))).isFalse();
349+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope4, v1))).isTrue();
349350
stepScope4.setStep(buildMoveScope(stepScope4, v0).getMove());
350351
acceptor.stepEnded(stepScope4);
351352
phaseScope.setLastCompletedStepScope(stepScope4);
352353

353-
solverScope.setWorkingRandom(new ai.timefold.solver.core.testutil.TestRandom(0.1));
354+
solverScope.setWorkingRandom(new TestRandom(0.99));
354355
var stepScope5 = new LocalSearchStepScope<>(phaseScope);
355-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope5, v1))).isTrue();
356+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope5, v1))).isFalse();
356357
stepScope5.setStep(buildMoveScope(stepScope5, v0).getMove());
357358
acceptor.stepEnded(stepScope5);
358359
phaseScope.setLastCompletedStepScope(stepScope5);
359360

360-
solverScope.setWorkingRandom(new ai.timefold.solver.core.testutil.TestRandom(0.99));
361+
solverScope.setWorkingRandom(new TestRandom(new double[0]));
361362
var stepScope6 = new LocalSearchStepScope<>(phaseScope);
362-
assertThat(acceptor.isAccepted(buildMoveScope(stepScope6, v1))).isFalse();
363+
assertThat(acceptor.isAccepted(buildMoveScope(stepScope6, v1))).isTrue();
363364
stepScope6.setStep(buildMoveScope(stepScope6, v0).getMove());
364365
acceptor.stepEnded(stepScope6);
365366
phaseScope.setLastCompletedStepScope(stepScope6);
366367

367-
solverScope.setWorkingRandom(new ai.timefold.solver.core.testutil.TestRandom(new double[0]));
368+
solverScope.setWorkingRandom(new TestRandom(new double[0]));
368369
var stepScope7 = new LocalSearchStepScope<>(phaseScope);
369370
assertThat(acceptor.isAccepted(buildMoveScope(stepScope7, v1))).isTrue();
370371

0 commit comments

Comments
 (0)