Skip to content
Merged
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 @@ -17,6 +17,7 @@
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.AbstractTabuAcceptor;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.MoveTabuAcceptor;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.ValueTabuAcceptor;
Expand Down Expand Up @@ -54,15 +55,14 @@ public Acceptor<Solution_> buildAcceptor(HeuristicConfigPolicy<Solution_> config
.collect(Collectors.toList());

if (acceptorList.size() == 1) {
return acceptorList.get(0);
return acceptorList.getFirst();
} else if (acceptorList.size() > 1) {
return new CompositeAcceptor<>(acceptorList);
} else {
throw new IllegalArgumentException(
"The acceptor does not specify any acceptorType (" + acceptorConfig.getAcceptorTypeList()
+ ") or other acceptor property.\n"
+ "For a good starting values,"
+ " see the docs section \"Which optimization algorithms should I use?\".");
throw new IllegalArgumentException("""
The acceptor does not specify any acceptorType (%s) or other acceptor property.
For good starting values, see the docs section "Which optimization algorithms should I use?"."""
.formatted(acceptorConfig.getAcceptorTypeList()));
}
}

Expand Down Expand Up @@ -94,34 +94,35 @@ private Optional<StepCountingHillClimbingAcceptor<Solution_>> buildStepCountingH
}

private Optional<EntityTabuAcceptor<Solution_>> buildEntityTabuAcceptor(HeuristicConfigPolicy<Solution_> configPolicy) {
var entityTabuSize = acceptorConfig.getEntityTabuSize();
var entityTabuRatio = acceptorConfig.getEntityTabuRatio();
var fadingEntityTabuSize = acceptorConfig.getFadingEntityTabuSize();
var fadingEntityTabuRatio = acceptorConfig.getFadingEntityTabuRatio();
if (acceptorTypeListsContainsAcceptorType(AcceptorType.ENTITY_TABU)
|| acceptorConfig.getEntityTabuSize() != null || acceptorConfig.getEntityTabuRatio() != null
|| acceptorConfig.getFadingEntityTabuSize() != null || acceptorConfig.getFadingEntityTabuRatio() != null) {
|| entityTabuSize != null || entityTabuRatio != null
|| fadingEntityTabuSize != null || fadingEntityTabuRatio != null) {
var acceptor = new EntityTabuAcceptor<Solution_>(configPolicy.getLogIndentation());
if (acceptorConfig.getEntityTabuSize() != null) {
if (acceptorConfig.getEntityTabuRatio() != null) {
throw new IllegalArgumentException("The acceptor cannot have both acceptorConfig.getEntityTabuSize() ("
+ acceptorConfig.getEntityTabuSize() + ") and acceptorConfig.getEntityTabuRatio() ("
+ acceptorConfig.getEntityTabuRatio() + ").");
if (entityTabuSize != null) {
if (entityTabuRatio != null) {
throw new IllegalArgumentException(
"The acceptor cannot have both entityTabuSize (%d) and entityTabuRatio (%f)."
.formatted(entityTabuSize, entityTabuRatio));
}
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getEntityTabuSize()));
} else if (acceptorConfig.getEntityTabuRatio() != null) {
acceptor.setTabuSizeStrategy(new EntityRatioTabuSizeStrategy<>(acceptorConfig.getEntityTabuRatio()));
} else if (acceptorConfig.getFadingEntityTabuSize() == null && acceptorConfig.getFadingEntityTabuRatio() == null) {
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(entityTabuSize));
} else if (entityTabuRatio != null) {
acceptor.setTabuSizeStrategy(new EntityRatioTabuSizeStrategy<>(entityTabuRatio));
} else if (fadingEntityTabuSize == null && fadingEntityTabuRatio == null) {
acceptor.setTabuSizeStrategy(new EntityRatioTabuSizeStrategy<>(0.1));
}
if (acceptorConfig.getFadingEntityTabuSize() != null) {
if (acceptorConfig.getFadingEntityTabuRatio() != null) {
if (fadingEntityTabuSize != null) {
if (fadingEntityTabuRatio != null) {
throw new IllegalArgumentException(
"The acceptor cannot have both acceptorConfig.getFadingEntityTabuSize() ("
+ acceptorConfig.getFadingEntityTabuSize()
+ ") and acceptorConfig.getFadingEntityTabuRatio() ("
+ acceptorConfig.getFadingEntityTabuRatio() + ").");
"The acceptor cannot have both fadingEntityTabuSize (%d) and fadingEntityTabuRatio (%f)."
.formatted(fadingEntityTabuSize, fadingEntityTabuRatio));
}
acceptor.setFadingTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getFadingEntityTabuSize()));
} else if (acceptorConfig.getFadingEntityTabuRatio() != null) {
acceptor.setFadingTabuSizeStrategy(
new EntityRatioTabuSizeStrategy<>(acceptorConfig.getFadingEntityTabuRatio()));
acceptor.setFadingTabuSizeStrategy(new FixedTabuSizeStrategy<>(fadingEntityTabuSize));
} else if (fadingEntityTabuRatio != null) {
acceptor.setFadingTabuSizeStrategy(new EntityRatioTabuSizeStrategy<>(fadingEntityTabuRatio));
}
if (configPolicy.getEnvironmentMode().isFullyAsserted()) {
acceptor.setAssertTabuHashCodeCorrectness(true);
Expand All @@ -132,43 +133,47 @@ private Optional<EntityTabuAcceptor<Solution_>> buildEntityTabuAcceptor(Heuristi
}

private Optional<ValueTabuAcceptor<Solution_>> buildValueTabuAcceptor(HeuristicConfigPolicy<Solution_> configPolicy) {
var valueTabuSize = acceptorConfig.getValueTabuSize();
var fadingValueTabuSize = acceptorConfig.getFadingValueTabuSize();
if (acceptorTypeListsContainsAcceptorType(AcceptorType.VALUE_TABU)
|| acceptorConfig.getValueTabuSize() != null || acceptorConfig.getFadingValueTabuSize() != null) {
var acceptor = new ValueTabuAcceptor<Solution_>(configPolicy.getLogIndentation());
if (acceptorConfig.getValueTabuSize() != null) {
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getValueTabuSize()));
}
if (acceptorConfig.getFadingValueTabuSize() != null) {
acceptor.setFadingTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getFadingValueTabuSize()));
}

if (acceptorConfig.getValueTabuSize() != null) {
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getValueTabuSize()));
}
if (acceptorConfig.getFadingValueTabuSize() != null) {
acceptor.setFadingTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getFadingValueTabuSize()));
}
if (configPolicy.getEnvironmentMode().isFullyAsserted()) {
acceptor.setAssertTabuHashCodeCorrectness(true);
|| valueTabuSize != null || fadingValueTabuSize != null) {
if (valueTabuSize == null && fadingValueTabuSize == null) {
throw new IllegalArgumentException(
"The acceptorType (%s) requires either valueTabuSize or fadingValueTabuSize to be configured."
.formatted(AcceptorType.VALUE_TABU));
}
var acceptor = new ValueTabuAcceptor<Solution_>(configPolicy.getLogIndentation());
configureFixedSizeTabuAcceptor(acceptor, configPolicy, valueTabuSize, fadingValueTabuSize);
return Optional.of(acceptor);
}
return Optional.empty();
}

private static <Solution_> void configureFixedSizeTabuAcceptor(AbstractTabuAcceptor<Solution_> acceptor,
HeuristicConfigPolicy<Solution_> configPolicy, Integer tabuSize, Integer fadingTabuSize) {
if (tabuSize != null) {
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(tabuSize));
}
if (fadingTabuSize != null) {
acceptor.setFadingTabuSizeStrategy(new FixedTabuSizeStrategy<>(fadingTabuSize));
}
if (configPolicy.getEnvironmentMode().isFullyAsserted()) {
acceptor.setAssertTabuHashCodeCorrectness(true);
}
}

private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicConfigPolicy<Solution_> configPolicy) {
var moveTabuSize = acceptorConfig.getMoveTabuSize();
var fadingMoveTabuSize = acceptorConfig.getFadingMoveTabuSize();
if (acceptorTypeListsContainsAcceptorType(AcceptorType.MOVE_TABU)
|| acceptorConfig.getMoveTabuSize() != null || acceptorConfig.getFadingMoveTabuSize() != null) {
var acceptor = new MoveTabuAcceptor<Solution_>(configPolicy.getLogIndentation());
if (acceptorConfig.getMoveTabuSize() != null) {
acceptor.setTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getMoveTabuSize()));
}
if (acceptorConfig.getFadingMoveTabuSize() != null) {
acceptor.setFadingTabuSizeStrategy(new FixedTabuSizeStrategy<>(acceptorConfig.getFadingMoveTabuSize()));
}
if (configPolicy.getEnvironmentMode().isFullyAsserted()) {
acceptor.setAssertTabuHashCodeCorrectness(true);
|| moveTabuSize != null || fadingMoveTabuSize != null) {
if (moveTabuSize == null && fadingMoveTabuSize == null) {
throw new IllegalArgumentException(
"The acceptorType (%s) requires either moveTabuSize or fadingMoveTabuSize to be configured."
.formatted(AcceptorType.MOVE_TABU));
}
var acceptor = new MoveTabuAcceptor<Solution_>(configPolicy.getLogIndentation());
configureFixedSizeTabuAcceptor(acceptor, configPolicy, moveTabuSize, fadingMoveTabuSize);
return Optional.of(acceptor);
}
return Optional.empty();
Expand All @@ -181,9 +186,9 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
var acceptor = new SimulatedAnnealingAcceptor<Solution_>();
if (acceptorConfig.getSimulatedAnnealingStartingTemperature() == null) {
// TODO Support SA without a parameter
throw new IllegalArgumentException("The acceptorType (" + AcceptorType.SIMULATED_ANNEALING
+ ") currently requires a acceptorConfig.getSimulatedAnnealingStartingTemperature() ("
+ acceptorConfig.getSimulatedAnnealingStartingTemperature() + ").");
throw new IllegalArgumentException(
"The acceptorType (%s) requires non-null acceptorConfig.getSimulatedAnnealingStartingTemperature()."
.formatted(AcceptorType.SIMULATED_ANNEALING));
}
acceptor.setStartingTemperature(
configPolicy.getScoreDefinition().parseScore(acceptorConfig.getSimulatedAnnealingStartingTemperature()));
Expand Down Expand Up @@ -221,19 +226,20 @@ private Optional<GreatDelugeAcceptor<Solution_>> buildGreatDelugeAcceptor(Heuris
var acceptor = new GreatDelugeAcceptor<Solution_>();
if (acceptorConfig.getGreatDelugeWaterLevelIncrementScore() != null) {
if (acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() != null) {
throw new IllegalArgumentException("The acceptor cannot have both a "
+ "acceptorConfig.getGreatDelugeWaterLevelIncrementScore() ("
+ acceptorConfig.getGreatDelugeWaterLevelIncrementScore()
+ ") and a acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() ("
+ acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() + ").");
throw new IllegalArgumentException("""
The acceptor cannot have both acceptorConfig.getGreatDelugeWaterLevelIncrementScore() (%s) \
and acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() (%s)."""
.formatted(acceptorConfig.getGreatDelugeWaterLevelIncrementScore(),
acceptorConfig.getGreatDelugeWaterLevelIncrementRatio()));
}
acceptor.setWaterLevelIncrementScore(
configPolicy.getScoreDefinition().parseScore(acceptorConfig.getGreatDelugeWaterLevelIncrementScore()));
} else if (acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() != null) {
if (acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() <= 0.0) {
throw new IllegalArgumentException("The acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() ("
+ acceptorConfig.getGreatDelugeWaterLevelIncrementRatio()
+ ") must be positive because the water level should increase.");
throw new IllegalArgumentException("""
The acceptorConfig.getGreatDelugeWaterLevelIncrementRatio() (%s) must be positive \
because the water level should increase."""
.formatted(acceptorConfig.getGreatDelugeWaterLevelIncrementRatio()));
}
acceptor.setWaterLevelIncrementRatio(acceptorConfig.getGreatDelugeWaterLevelIncrementRatio());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,14 @@ public boolean isAccepted(LocalSearchMoveScope<Solution_> moveScope) {
logIndentation, moveScope.getMove());
return false;
}
var acceptChance = calculateFadingTabuAcceptChance(tabuStepCount - workingTabuSize);
var accepted = moveScope.getWorkingRandom().nextDouble() < acceptChance;
var decision = decideFadingTabuAcceptance(moveScope, tabuStepCount - workingTabuSize);
var accepted = decision > 0;
if (accepted) {
logger.trace("{} Proposed move ({}) is fading tabu with acceptChance ({}) and is accepted.",
logIndentation,
moveScope.getMove(), acceptChance);
logIndentation, moveScope.getMove(), Math.abs(decision));
} else {
logger.trace("{} Proposed move ({}) is fading tabu with acceptChance ({}) and is not accepted.",
logIndentation,
moveScope.getMove(), acceptChance);
logIndentation, moveScope.getMove(), Math.abs(decision));
}
return accepted;
}
Expand Down Expand Up @@ -185,12 +183,24 @@ private int locateMaximumTabuStepIndex(LocalSearchMoveScope<Solution_> moveScope

/**
* @param fadingTabuStepCount {@code 0 < fadingTabuStepCount <= fadingTabuSize}
* @return {@code 0.0 < acceptChance < 1.0}
* @return in absolute value, the accept chance;
* negative signum or 0 means not accepted, positive signum means accepted.
* This is hacky, but we can represent 2 things with one number
* and avoid allocation new types for this multiple return.
*/
protected double calculateFadingTabuAcceptChance(int fadingTabuStepCount) {
// The + 1's are because acceptChance should not be 0.0 or 1.0
// when (fadingTabuStepCount == 0) or (fadingTabuStepCount + 1 == workingFadingTabuSize)
return (workingFadingTabuSize - fadingTabuStepCount) / ((double) (workingFadingTabuSize + 1));
private double decideFadingTabuAcceptance(LocalSearchMoveScope<Solution_> moveScope, int fadingTabuStepCount) {
// Invert the chance; the longer the element is in the tabu list, the higher the chance should be.
var numerator = workingFadingTabuSize - fadingTabuStepCount;
if (numerator <= 0) { // The inverted chance would be >= 1.
return 1.0d;
}
var denominator = workingFadingTabuSize + 1;
if (numerator >= denominator) { // The inverted chance would be <= 0.
return 0.0d;
}
var acceptChance = 1.0d - (numerator / (double) denominator);
var accepted = moveScope.getWorkingRandom().nextDouble() < acceptChance;
return accepted ? acceptChance : -acceptChance;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ public final class FixedTabuSizeStrategy<Solution_> extends AbstractTabuSizeStra

public FixedTabuSizeStrategy(int tabuSize) {
this.tabuSize = tabuSize;
if (tabuSize < 0) {
throw new IllegalArgumentException("The tabuSize (" + tabuSize
+ ") cannot be negative.");
if (tabuSize < 1) {
throw new IllegalArgumentException("The tabuSize (%d) must be at least 1."
.formatted(tabuSize));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,22 @@ <Solution_> void diversifiedLateAcceptanceAcceptor() {
AcceptorFactory<Solution_> badAcceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig);
assertThatIllegalStateException().isThrownBy(() -> badAcceptorFactory.buildAcceptor(heuristicConfigPolicy));
}

@Test
<Solution_> void valueTabuWithoutSizes_throwsException() {
var config = new LocalSearchAcceptorConfig()
.withAcceptorTypeList(List.of(AcceptorType.VALUE_TABU));
var factory = AcceptorFactory.create(config);
assertThatIllegalArgumentException()
.isThrownBy(() -> factory.buildAcceptor(mock(HeuristicConfigPolicy.class)));
}

@Test
<Solution_> void moveTabuWithoutSizes_throwsException() {
var config = new LocalSearchAcceptorConfig()
.withAcceptorTypeList(List.of(AcceptorType.MOVE_TABU));
var factory = AcceptorFactory.create(config);
assertThatIllegalArgumentException()
.isThrownBy(() -> factory.buildAcceptor(mock(HeuristicConfigPolicy.class)));
}
}
Loading
Loading