Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,15 @@ protected void adjustTabuList(int tabuStepIndex, Collection<@Nullable Object> ta
}
// Add the new tabu(s)
for (var tabu : tabus) {
// Push tabu to the end of the line; remove+put has that effect in LinkedHashMap.
tabuToStepIndexMap.remove(tabu);
// Skip null planning values (unassigned state)
if (tabu == null) {
continue;
}
// Push tabu to the end of the line
if (tabuToStepIndexMap.containsKey(tabu)) {
tabuToStepIndexMap.remove(tabu);
tabuSequenceDeque.remove(tabu);
}
tabuToStepIndexMap.put(tabu, tabuStepIndex);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import ai.timefold.solver.core.api.score.SimpleScore;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.size.FixedTabuSizeStrategy;
Expand Down Expand Up @@ -253,7 +255,9 @@ private static <Solution_> LocalSearchMoveScope<Solution_> buildMoveScope(
}

@Test
void unassignedPlanningValue() {
void nullablePlanningValue() {
// ValueTabuAcceptor should handle null planning values (nullable planning variables)
// Previously threw NullPointerException when trying to add null to ArrayDeque
var acceptor = new ValueTabuAcceptor<>("");
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(2));
acceptor.setAspirationEnabled(true);
Expand All @@ -268,115 +272,43 @@ void unassignedPlanningValue() {

var stepScope0 = new LocalSearchStepScope<>(phaseScope);

// Build a move scope with a null planning value
var moveScopeWithNull = buildMoveScope(stepScope0, 0, v0, null, v1);
// Build a move scope with a null planning value (nullable planning variable)
var moveScopeWithNull = buildMoveScopeWithNull(stepScope0, v0, null, v1);

// Should accept the move (no tabu yet)
assertThat(acceptor.isAccepted(moveScopeWithNull)).isTrue();

// stepEnded() calls adjustTabuList() which fills the tabu list
// After fix: should NOT throw NullPointerException
// stepEnded() calls adjustTabuList() which should skip null values
stepScope0.setStep(moveScopeWithNull.getMove());
acceptor.stepEnded(stepScope0);
phaseScope.setLastCompletedStepScope(stepScope0);

// Null value should not be accepted (they are still tracked as tabu)
// Verify that v0 and v1 are now tabu, but null was skipped
var stepScope1 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope1, 0, v0))).isFalse();
assertThat(acceptor.isAccepted(buildMoveScope(stepScope1, 0, v1))).isFalse();
assertThat(acceptor.isAccepted(buildMoveScope(stepScope1, 0, new TestdataValue[] { null }))).isFalse();

// push the null out of the tabu list
stepScope1.setStep(buildMoveScope(stepScope1, v0).getMove());
acceptor.stepEnded(stepScope1);
phaseScope.setLastCompletedStepScope(stepScope1);

var stepScope2 = new LocalSearchStepScope<>(phaseScope);
stepScope2.setStep(buildMoveScope(stepScope2, v1).getMove());
acceptor.stepEnded(stepScope2);
phaseScope.setLastCompletedStepScope(stepScope2);

// Null value should be accepted, as the moves pushed it out of the tabu list.
var stepScope3 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope3, 0, v0))).isFalse();
assertThat(acceptor.isAccepted(buildMoveScope(stepScope3, 0, v1))).isFalse();
assertThat(acceptor.isAccepted(buildMoveScope(stepScope3, 0, new TestdataValue[] { null }))).isTrue();
assertThat(acceptor.isAccepted(buildMoveScopeWithNull(stepScope1, v0))).isFalse();
assertThat(acceptor.isAccepted(buildMoveScopeWithNull(stepScope1, v1))).isFalse();
// null values should still be accepted (they are not tracked as tabu)
assertThat(acceptor.isAccepted(buildMoveScopeWithNull(stepScope1, null))).isTrue();

acceptor.phaseEnded(phaseScope);
}

@Test
void fadingTabuSize() {
var acceptor = new ValueTabuAcceptor<>("");
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(2));
acceptor.setFadingTabuSizeStrategy(new FixedTabuSizeStrategy<>(4));

var v0 = new TestdataValue("v0");
var v1 = new TestdataValue("v1");

var solverScope = new SolverScope<>();
solverScope.setInitializedBestScore(SimpleScore.ZERO);
solverScope.setWorkingRandom(new TestRandom(new double[0]));
var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0);
acceptor.phaseStarted(phaseScope);

// Step 0: tabu v1 at stepIndex=0
var stepScope0 = new LocalSearchStepScope<>(phaseScope);
stepScope0.setStep(buildMoveScope(stepScope0, v1).getMove());
acceptor.stepEnded(stepScope0);
phaseScope.setLastCompletedStepScope(stepScope0);

// Steps 1-2: hard tabu (tabuStepCount 1,2 ≤ 2) — no random consumed
var stepScope1 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope1, v1))).isFalse();
assertThat(acceptor.isAccepted(buildMoveScope(stepScope1, v0))).isTrue();
stepScope1.setStep(buildMoveScope(stepScope1, v0).getMove());
acceptor.stepEnded(stepScope1);
phaseScope.setLastCompletedStepScope(stepScope1);

var stepScope2 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope2, v1))).isFalse();
stepScope2.setStep(buildMoveScope(stepScope2, v0).getMove());
acceptor.stepEnded(stepScope2);
phaseScope.setLastCompletedStepScope(stepScope2);

// Step 3: fading zone, fadingCount=1, acceptChance=0.4; random=0.5 → rejected
solverScope.setWorkingRandom(new TestRandom(0.5));
var stepScope3 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope3, v1))).isFalse();
stepScope3.setStep(buildMoveScope(stepScope3, v0).getMove());
acceptor.stepEnded(stepScope3);
phaseScope.setLastCompletedStepScope(stepScope3);

// Step 4: fading zone, fadingCount=2, acceptChance=0.6; random=0.5 → accepted
solverScope.setWorkingRandom(new TestRandom(0.5));
var stepScope4 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope4, v1))).isTrue();
stepScope4.setStep(buildMoveScope(stepScope4, v0).getMove());
acceptor.stepEnded(stepScope4);
phaseScope.setLastCompletedStepScope(stepScope4);

// Step 5: fading zone, fadingCount=3, acceptChance=0.8; random=0.5 → accepted
solverScope.setWorkingRandom(new TestRandom(0.5));
var stepScope5 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope5, v1))).isTrue();
stepScope5.setStep(buildMoveScope(stepScope5, v0).getMove());
acceptor.stepEnded(stepScope5);
phaseScope.setLastCompletedStepScope(stepScope5);

// Step 6: fading zone, fadingCount=4, acceptChance=1.0; random not consumed and accepted
solverScope.setWorkingRandom(new TestRandom(new double[0]));
var stepScope6 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope6, v1))).isTrue();
stepScope6.setStep(buildMoveScope(stepScope6, v0).getMove());
acceptor.stepEnded(stepScope6);
phaseScope.setLastCompletedStepScope(stepScope6);

// Step 7: v1 expired, no random consumed
solverScope.setWorkingRandom(new TestRandom(new double[0]));
var stepScope7 = new LocalSearchStepScope<>(phaseScope);
assertThat(acceptor.isAccepted(buildMoveScope(stepScope7, v1))).isTrue();

acceptor.phaseEnded(phaseScope);
private static <Solution_> LocalSearchMoveScope<Solution_> buildMoveScopeWithNull(
LocalSearchStepScope<Solution_> stepScope, TestdataValue... values) {
var move = mock(Move.class);
// Create a list that may contain null values (ArrayList explicitly allows null)
List<TestdataValue> valueList = new ArrayList<>();
// Handle single-null varargs edge case: method(null) passes null as the array, not [null]
if (values != null) {
for (TestdataValue v : values) {
valueList.add(v);
}
}
when(move.getPlanningValues()).thenReturn(valueList);
var moveScope = new LocalSearchMoveScope<Solution_>(stepScope, 0, move);
moveScope.setInitializedScore(SimpleScore.ZERO);
return moveScope;
}

}
Loading