diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 1549f7cff7..0bcae1ee66 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -317,6 +317,9 @@ + + + @@ -2543,6 +2546,21 @@ + + + + + + + + + + + + + + + @@ -3062,6 +3080,9 @@ + + + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index d348497b80..6a6ed1bd75 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -80,6 +80,17 @@ "old": "method Score_ ai.timefold.solver.core.api.score.constraint.ConstraintMatch>::getScore()", "new": "method Score_ ai.timefold.solver.core.api.score.constraint.ConstraintMatch>::getScore()", "justification": "False positive after addition of @NonNull annotation" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.solver.SolverConfig", + "new": "class ai.timefold.solver.core.config.solver.SolverConfig", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"environmentMode\", \"daemon\", \"randomType\", \"randomSeed\", \"randomFactoryClass\", \"moveThreadCount\", \"moveThreadBufferSize\", \"threadFactoryClass\", \"monitoringConfig\", \"solutionClass\", \"entityClassList\", \"domainAccessType\", \"scoreDirectorFactoryConfig\", \"terminationConfig\", \"phaseConfigList\"}", + "newValue": "{\"enablePreviewFeatureSet\", \"environmentMode\", \"daemon\", \"randomType\", \"randomSeed\", \"randomFactoryClass\", \"moveThreadCount\", \"moveThreadBufferSize\", \"threadFactoryClass\", \"monitoringConfig\", \"solutionClass\", \"entityClassList\", \"domainAccessType\", \"scoreDirectorFactoryConfig\", \"terminationConfig\", \"nearbyDistanceMeterClass\", \"phaseConfigList\"}", + "justification": "Enable features preview config" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/AcceptorType.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/AcceptorType.java index e6a3d92bb1..6a2a34e9de 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/AcceptorType.java +++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/AcceptorType.java @@ -11,6 +11,7 @@ public enum AcceptorType { UNDO_MOVE_TABU, SIMULATED_ANNEALING, LATE_ACCEPTANCE, + DIVERSIFIED_LATE_ACCEPTANCE, GREAT_DELUGE, STEP_COUNTING_HILL_CLIMBING } diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java new file mode 100644 index 0000000000..bf5b8ffcef --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java @@ -0,0 +1,5 @@ +package ai.timefold.solver.core.config.solver; + +public enum PreviewFeature { + DIVERSIFIED_LATE_ACCEPTANCE +} diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java index 631493683a..bc2c86b138 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java @@ -11,9 +11,11 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ThreadFactory; import java.util.function.Consumer; @@ -60,6 +62,7 @@ */ @XmlRootElement(name = SolverConfig.XML_ELEMENT_NAME) @XmlType(name = SolverConfig.XML_TYPE_NAME, propOrder = { + "enablePreviewFeatureSet", "environmentMode", "daemon", "randomType", @@ -209,7 +212,8 @@ public class SolverConfig extends AbstractConfig { // Warning: all fields are null (and not defaulted) because they can be inherited // and also because the input config file should match the output config file - + @XmlElement(name = "enablePreviewFeature") + protected Set enablePreviewFeatureSet = null; protected EnvironmentMode environmentMode = null; protected Boolean daemon = null; protected RandomType randomType = null; @@ -284,6 +288,14 @@ public void setClassLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; } + public @Nullable Set getEnablePreviewFeatureSet() { + return enablePreviewFeatureSet; + } + + public void setEnablePreviewFeatureSet(@Nullable Set enablePreviewFeatureSet) { + this.enablePreviewFeatureSet = enablePreviewFeatureSet; + } + public @Nullable EnvironmentMode getEnvironmentMode() { return environmentMode; } @@ -432,6 +444,11 @@ public void setMonitoringConfig(@Nullable MonitoringConfig monitoringConfig) { // With methods // ************************************************************************ + public @NonNull SolverConfig withPreviewFeature(@NonNull PreviewFeature... previewFeature) { + enablePreviewFeatureSet = EnumSet.copyOf(Arrays.asList(previewFeature)); + return this; + } + public @NonNull SolverConfig withEnvironmentMode(@NonNull EnvironmentMode environmentMode) { this.environmentMode = environmentMode; return this; @@ -636,10 +653,8 @@ public boolean canTerminate() { // ************************************************************************ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) { - if (environmentMode == null || environmentMode.isReproducible()) { - if (randomFactoryClass == null && randomSeed == null) { - randomSeed = subSingleIndex; - } + if ((environmentMode == null || environmentMode.isReproducible()) && randomFactoryClass == null && randomSeed == null) { + randomSeed = subSingleIndex; } } @@ -650,6 +665,8 @@ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) { @Override public @NonNull SolverConfig inherit(@NonNull SolverConfig inheritedConfig) { classLoader = ConfigUtils.inheritOverwritableProperty(classLoader, inheritedConfig.getClassLoader()); + enablePreviewFeatureSet = ConfigUtils.inheritMergeableEnumSetProperty(enablePreviewFeatureSet, + inheritedConfig.getEnablePreviewFeatureSet()); environmentMode = ConfigUtils.inheritOverwritableProperty(environmentMode, inheritedConfig.getEnvironmentMode()); daemon = ConfigUtils.inheritOverwritableProperty(daemon, inheritedConfig.getDaemon()); randomType = ConfigUtils.inheritOverwritableProperty(randomType, inheritedConfig.getRandomType()); diff --git a/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java b/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java index 8ccd764712..6ab894e062 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java +++ b/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -211,6 +212,19 @@ public static void applyCustomProperties(@NonNull Object bean, @NonNull String b } } + public static > @Nullable Set inheritMergeableEnumSetProperty(@Nullable Set originalSet, + @Nullable Set inheritedSet) { + if (inheritedSet == null) { + return originalSet; + } else if (originalSet == null) { + return EnumSet.copyOf(inheritedSet); + } else { + var newSet = EnumSet.copyOf(originalSet); + newSet.addAll(inheritedSet); + return newSet; + } + } + public static @Nullable List inheritUniqueMergeableListProperty(@Nullable List originalList, @Nullable List inheritedList) { if (inheritedList == null) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java index 10906c46b7..88ec3198e1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java @@ -3,11 +3,13 @@ import java.util.HashMap; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ThreadFactory; import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner; import ai.timefold.solver.core.config.solver.EnvironmentMode; +import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; @@ -25,6 +27,7 @@ public class HeuristicConfigPolicy { + private final Set previewFeatureList; private final EnvironmentMode environmentMode; private final String logIndentation; private final Integer moveThreadCount; @@ -46,6 +49,7 @@ public class HeuristicConfigPolicy { private final Map> valueMimicRecorderMap = new HashMap<>(); private HeuristicConfigPolicy(Builder builder) { + this.previewFeatureList = builder.previewFeatureList; this.environmentMode = builder.environmentMode; this.logIndentation = builder.logIndentation; this.moveThreadCount = builder.moveThreadCount; @@ -128,8 +132,17 @@ public Random getRandom() { // ************************************************************************ public Builder cloneBuilder() { - return new Builder<>(environmentMode, moveThreadCount, moveThreadBufferSize, threadFactoryClass, - nearbyDistanceMeterClass, random, initializingScoreTrend, solutionDescriptor, classInstanceCache) + return new Builder() + .withPreviewFeatureList(previewFeatureList) + .withEnvironmentMode(environmentMode) + .withMoveThreadCount(moveThreadCount) + .withMoveThreadBufferSize(moveThreadBufferSize) + .withThreadFactoryClass(threadFactoryClass) + .withNearbyDistanceMeterClass(nearbyDistanceMeterClass) + .withRandom(random) + .withInitializingScoreTrend(initializingScoreTrend) + .withSolutionDescriptor(solutionDescriptor) + .withClassInstanceCache(classInstanceCache) .withLogIndentation(logIndentation); } @@ -148,11 +161,13 @@ public HeuristicConfigPolicy createChildThreadConfigPolicy(ChildThrea // ************************************************************************ public void addEntityMimicRecorder(String id, EntityMimicRecorder mimicRecordingEntitySelector) { - EntityMimicRecorder put = entityMimicRecorderMap.put(id, mimicRecordingEntitySelector); + var put = entityMimicRecorderMap.put(id, mimicRecordingEntitySelector); if (put != null) { - throw new IllegalStateException("Multiple " + EntityMimicRecorder.class.getSimpleName() + "s (usually " - + EntitySelector.class.getSimpleName() + "s) have the same id (" + id + ").\n" + - "Maybe specify a variable name for the mimicking selector in situations with multiple variables on the same entity?"); + throw new IllegalStateException( + """ + Multiple %ss (usually %ss) have the same id (%s). + Maybe specify a variable name for the mimicking selector in situations with multiple variables on the same entity?""" + .formatted(EntityMimicRecorder.class.getSimpleName(), EntitySelector.class.getSimpleName(), id)); } } @@ -161,11 +176,13 @@ public EntityMimicRecorder getEntityMimicRecorder(String id) { } public void addSubListMimicRecorder(String id, SubListMimicRecorder mimicRecordingSubListSelector) { - SubListMimicRecorder put = subListMimicRecorderMap.put(id, mimicRecordingSubListSelector); + var put = subListMimicRecorderMap.put(id, mimicRecordingSubListSelector); if (put != null) { - throw new IllegalStateException("Multiple " + SubListMimicRecorder.class.getSimpleName() + "s (usually " - + SubListSelector.class.getSimpleName() + "s) have the same id (" + id + ").\n" + - "Maybe specify a variable name for the mimicking selector in situations with multiple variables on the same entity?"); + throw new IllegalStateException( + """ + Multiple %ss (usually %ss) have the same id (%s). + Maybe specify a variable name for the mimicking selector in situations with multiple variables on the same entity?""" + .formatted(SubListMimicRecorder.class.getSimpleName(), SubListSelector.class.getSimpleName(), id)); } } @@ -174,11 +191,13 @@ public SubListMimicRecorder getSubListMimicRecorder(String id) { } public void addValueMimicRecorder(String id, ValueMimicRecorder mimicRecordingValueSelector) { - ValueMimicRecorder put = valueMimicRecorderMap.put(id, mimicRecordingValueSelector); + var put = valueMimicRecorderMap.put(id, mimicRecordingValueSelector); if (put != null) { - throw new IllegalStateException("Multiple " + ValueMimicRecorder.class.getSimpleName() + "s (usually " - + ValueSelector.class.getSimpleName() + "s) have the same id (" + id + ").\n" + - "Maybe specify a variable name for the mimicking selector in situations with multiple variables on the same entity?"); + throw new IllegalStateException( + """ + Multiple %ss (usually %ss) have the same id (%s). + Maybe specify a variable name for the mimicking selector in situations with multiple variables on the same entity?""" + .formatted(ValueMimicRecorder.class.getSimpleName(), ValueSelector.class.getSimpleName(), id)); } } @@ -198,6 +217,16 @@ public ThreadFactory buildThreadFactory(ChildThreadType childThreadType) { } } + public void ensurePreviewFeature(PreviewFeature previewFeature) { + if (previewFeatureList == null || !previewFeatureList.contains(previewFeature)) { + throw new IllegalStateException( + """ + The preview feature %s is not enabled. + Maybe add %s to in your configuration file?""" + .formatted(previewFeature, previewFeature)); + } + } + @Override public String toString() { return getClass().getSimpleName() + "(" + environmentMode + ")"; @@ -205,13 +234,14 @@ public String toString() { public static class Builder { - private final EnvironmentMode environmentMode; - private final Integer moveThreadCount; - private final Integer moveThreadBufferSize; - private final Class threadFactoryClass; - private final InitializingScoreTrend initializingScoreTrend; - private final SolutionDescriptor solutionDescriptor; - private final ClassInstanceCache classInstanceCache; + private Set previewFeatureList; + private EnvironmentMode environmentMode; + private Integer moveThreadCount; + private Integer moveThreadBufferSize; + private Class threadFactoryClass; + private InitializingScoreTrend initializingScoreTrend; + private SolutionDescriptor solutionDescriptor; + private ClassInstanceCache classInstanceCache; private String logIndentation = ""; @@ -222,23 +252,58 @@ public static class Builder { private boolean initializedChainedValueFilterEnabled = false; private boolean unassignedValuesAllowed = false; - private final Class> nearbyDistanceMeterClass; - private final Random random; + private Class> nearbyDistanceMeterClass; + private Random random; + + public Builder withPreviewFeatureList(Set previewFeatureList) { + this.previewFeatureList = previewFeatureList; + return this; + } - public Builder(EnvironmentMode environmentMode, Integer moveThreadCount, Integer moveThreadBufferSize, - Class threadFactoryClass, - Class> nearbyDistanceMeterClass, Random random, - InitializingScoreTrend initializingScoreTrend, SolutionDescriptor solutionDescriptor, - ClassInstanceCache classInstanceCache) { + public Builder withEnvironmentMode(EnvironmentMode environmentMode) { this.environmentMode = environmentMode; + return this; + } + + public Builder withMoveThreadCount(Integer moveThreadCount) { this.moveThreadCount = moveThreadCount; + return this; + } + + public Builder withMoveThreadBufferSize(Integer moveThreadBufferSize) { this.moveThreadBufferSize = moveThreadBufferSize; + return this; + } + + public Builder withThreadFactoryClass(Class threadFactoryClass) { this.threadFactoryClass = threadFactoryClass; + return this; + } + + public Builder + withNearbyDistanceMeterClass(Class> nearbyDistanceMeterClass) { this.nearbyDistanceMeterClass = nearbyDistanceMeterClass; + return this; + } + + public Builder withRandom(Random random) { this.random = random; + return this; + } + + public Builder withInitializingScoreTrend(InitializingScoreTrend initializingScoreTrend) { this.initializingScoreTrend = initializingScoreTrend; + return this; + } + + public Builder withSolutionDescriptor(SolutionDescriptor solutionDescriptor) { this.solutionDescriptor = solutionDescriptor; + return this; + } + + public Builder withClassInstanceCache(ClassInstanceCache classInstanceCache) { this.classInstanceCache = classInstanceCache; + return this; } public Builder withLogIndentation(String logIndentation) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index 964816af61..f1d2232668 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -9,9 +9,11 @@ import ai.timefold.solver.core.config.localsearch.decider.acceptor.AcceptorType; import ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig; import ai.timefold.solver.core.config.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingType; +import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.greatdeluge.GreatDelugeAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; 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; @@ -45,6 +47,7 @@ public Acceptor buildAcceptor(HeuristicConfigPolicy config buildMoveTabuAcceptor(configPolicy), buildSimulatedAnnealingAcceptor(configPolicy), buildLateAcceptanceAcceptor(), + buildDiversifiedLateAcceptanceAcceptor(configPolicy), buildGreatDelugeAcceptor(configPolicy)) .filter(Optional::isPresent) .map(Optional::get) @@ -214,7 +217,8 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon private Optional> buildLateAcceptanceAcceptor() { if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) - || acceptorConfig.getLateAcceptanceSize() != null) { + || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) + && acceptorConfig.getLateAcceptanceSize() != null)) { var acceptor = new LateAcceptanceAcceptor(); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); @@ -222,6 +226,17 @@ private Optional> buildLateAcceptanceAcceptor( return Optional.empty(); } + private Optional> + buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { + if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { + configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); + var acceptor = new DiversifiedLateAcceptanceAcceptor(); + acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); + return Optional.of(acceptor); + } + return Optional.empty(); + } + private Optional> buildGreatDelugeAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.GREAT_DELUGE) || acceptorConfig.getGreatDelugeWaterLevelIncrementScore() != null diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java new file mode 100644 index 0000000000..b87373414b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -0,0 +1,113 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance; + +import java.util.Arrays; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptor; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; + +public class DiversifiedLateAcceptanceAcceptor extends AbstractAcceptor { + + // The worst score in the late elements list + protected Score lateWorse; + // Number of occurrences of lateWorse in the late elements + protected int lateWorseOccurrences = -1; + + protected int lateAcceptanceSize = -1; + + protected Score[] previousScores; + protected int lateScoreIndex = -1; + + public void setLateAcceptanceSize(int lateAcceptanceSize) { + this.lateAcceptanceSize = lateAcceptanceSize; + } + + // ************************************************************************ + // Worker methods + // ************************************************************************ + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + validate(); + previousScores = new Score[lateAcceptanceSize]; + var initialScore = phaseScope.getBestScore(); + Arrays.fill(previousScores, initialScore); + lateScoreIndex = 0; + lateWorseOccurrences = lateAcceptanceSize; + lateWorse = initialScore; + } + + private void validate() { + if (lateAcceptanceSize <= 0) { + throw new IllegalArgumentException( + "The lateAcceptanceSize (%d) cannot be negative or zero.".formatted(lateAcceptanceSize)); + } + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public boolean isAccepted(LocalSearchMoveScope moveScope) { + // The acceptance and replacement strategies are based on the work: + // Diversified Late Acceptance Search by M. Namazi, C. Sanderson, M. A. H. Newton, M. M. A. Polash, and A. Sattar + var moveScore = moveScope.getScore(); + var current = (Score) moveScope.getStepScope().getPhaseScope().getLastCompletedStepScope().getScore(); + var previous = current; + var accept = moveScore.compareTo(current) == 0 || moveScore.compareTo(lateWorse) > 0; + if (accept) { + current = moveScore; + } + var lateScore = previousScores[lateScoreIndex]; + // Improves the diversification to allow the next iterations to find a better solution + var currentScoreCmp = current.compareTo(lateScore); + var currentScoreWorse = currentScoreCmp < 0; + // Improves the intensification but avoids replacing values when the search falls into a plateau or local minima + var currentScoreBetter = currentScoreCmp > 0 && current.compareTo(previous) > 0; + if (currentScoreWorse || currentScoreBetter) { + updateLateScore(current); + } + lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; + return accept; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void updateLateScore(Score newScore) { + var newScoreWorseCmp = newScore.compareTo(lateWorse); + var lateScore = (Score) previousScores[lateScoreIndex]; + var lateScoreEqual = lateScore.compareTo(lateWorse) == 0; + if (newScoreWorseCmp < 0) { + this.lateWorse = newScore; + this.lateWorseOccurrences = 1; + } else if (lateScoreEqual && newScoreWorseCmp != 0) { + this.lateWorseOccurrences--; + } else if (!lateScoreEqual && newScoreWorseCmp == 0) { + this.lateWorseOccurrences++; + } + previousScores[lateScoreIndex] = newScore; + // Recompute the new lateWorse and the number of occurrences + if (lateWorseOccurrences == 0) { + lateWorse = previousScores[0]; + lateWorseOccurrences = 1; + for (var i = 1; i < lateAcceptanceSize; i++) { + Score previousScore = previousScores[i]; + var scoreCmp = previousScore.compareTo(lateWorse); + if (scoreCmp < 0) { + lateWorse = previousScores[i]; + lateWorseOccurrences = 1; + } else if (scoreCmp == 0) { + lateWorseOccurrences++; + } + } + } + } + + @Override + public void phaseEnded(LocalSearchPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + previousScores = null; + lateScoreIndex = -1; + lateWorse = null; + lateWorseOccurrences = -1; + } +} \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index fc0e39ca02..332a1e15a1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance; +import java.util.Arrays; + import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptor; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; @@ -11,7 +13,7 @@ public class LateAcceptanceAcceptor extends AbstractAcceptor[] previousScores; protected int lateScoreIndex = -1; public void setLateAcceptanceSize(int lateAcceptanceSize) { @@ -31,32 +33,29 @@ public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); validate(); previousScores = new Score[lateAcceptanceSize]; - Score initialScore = phaseScope.getBestScore(); - for (int i = 0; i < previousScores.length; i++) { - previousScores[i] = initialScore; - } + var initialScore = phaseScope.getBestScore(); + Arrays.fill(previousScores, initialScore); lateScoreIndex = 0; } private void validate() { if (lateAcceptanceSize <= 0) { - throw new IllegalArgumentException("The lateAcceptanceSize (" + lateAcceptanceSize - + ") cannot be negative or zero."); + throw new IllegalArgumentException( + "The lateAcceptanceSize (%d) cannot be negative or zero.".formatted(lateAcceptanceSize)); } } @Override + @SuppressWarnings("unchecked") public boolean isAccepted(LocalSearchMoveScope moveScope) { - Score moveScore = moveScope.getScore(); - Score lateScore = previousScores[lateScoreIndex]; + var moveScore = moveScope.getScore(); + var lateScore = previousScores[lateScoreIndex]; if (moveScore.compareTo(lateScore) >= 0) { return true; } if (hillClimbingEnabled) { - Score lastStepScore = moveScope.getStepScope().getPhaseScope().getLastCompletedStepScope().getScore(); - if (moveScore.compareTo(lastStepScore) >= 0) { - return true; - } + var lastStepScore = moveScope.getStepScope().getPhaseScope().getLastCompletedStepScope().getScore(); + return moveScore.compareTo(lastStepScore) >= 0; } return false; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index 77f1e76d20..96b94ae8b3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -114,17 +114,20 @@ public > InnerScoreDirectorFactory buildBestSolutionRecaller(environmentMode); var randomFactory = buildRandomFactory(environmentMode); + var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); - var configPolicy = new HeuristicConfigPolicy.Builder<>( - environmentMode, - moveThreadCount, - solverConfig.getMoveThreadBufferSize(), - solverConfig.getThreadFactoryClass(), - solverConfig.getNearbyDistanceMeterClass(), - randomFactory.createRandom(), - scoreDirectorFactory.getInitializingScoreTrend(), - solutionDescriptor, - ClassInstanceCache.create()).build(); + var configPolicy = new HeuristicConfigPolicy.Builder() + .withPreviewFeatureList(previewFeaturesEnabled) + .withEnvironmentMode(environmentMode) + .withMoveThreadCount(moveThreadCount) + .withMoveThreadBufferSize(solverConfig.getMoveThreadBufferSize()) + .withThreadFactoryClass(solverConfig.getThreadFactoryClass()) + .withNearbyDistanceMeterClass(solverConfig.getNearbyDistanceMeterClass()) + .withRandom(randomFactory.createRandom()) + .withInitializingScoreTrend(scoreDirectorFactory.getInitializingScoreTrend()) + .withSolutionDescriptor(solutionDescriptor) + .withClassInstanceCache(ClassInstanceCache.create()) + .build(); var basicPlumbingTermination = new BasicPlumbingTermination(isDaemon); var termination = buildTerminationConfig(basicPlumbingTermination, configPolicy, configOverride); var phaseList = buildPhaseList(configPolicy, bestSolutionRecaller, termination); diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 766a833033..fd299b1598 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -11,6 +11,8 @@ + + @@ -1541,6 +1543,16 @@ + + + + + + + + + + @@ -1867,6 +1879,8 @@ + + diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java index 6b14df39c2..b7fa6c0665 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java @@ -65,6 +65,7 @@ class SolverConfigTest { private static final String TEST_SOLVER_CONFIG_WITH_NAMESPACE = "testSolverConfigWithNamespace.xml"; private static final String TEST_SOLVER_CONFIG_WITHOUT_NAMESPACE = "testSolverConfigWithoutNamespace.xml"; + private static final String TEST_SOLVER_CONFIG_WITH_ENUM_SET = "testSolverConfigWithEnumSet.xml"; private final SolverConfigIO solverConfigIO = new SolverConfigIO(); @ParameterizedTest @@ -164,6 +165,15 @@ void withConstraintProviderClass() { .isEqualTo(DummyConstraintProvider.class); } + @Test + void withEnablePreviewFeatureList() { + var solverConfig = new SolverConfig(); + assertThat(solverConfig.getEnablePreviewFeatureSet()).isNull(); + solverConfig.withPreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); + assertThat(solverConfig.getEnablePreviewFeatureSet()) + .hasSameElementsAs(List.of(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE)); + } + @Test void withTerminationSpentLimit() { var solverConfig = new SolverConfig(); @@ -192,6 +202,14 @@ void inherit() { assertThat(inheritedSolverConfig).usingRecursiveComparison().isEqualTo(originalSolverConfig); } + @Test + void inheritEnumSet() { + var originalSolverConfig = readSolverConfig(TEST_SOLVER_CONFIG_WITH_ENUM_SET); + var inheritedSolverConfig = + new SolverConfig().inherit(originalSolverConfig); + assertThat(inheritedSolverConfig).usingRecursiveComparison().isEqualTo(originalSolverConfig); + } + @Test void visitReferencedClasses() { var solverConfig = readSolverConfig(TEST_SOLVER_CONFIG_WITHOUT_NAMESPACE); @@ -380,18 +398,18 @@ public static class DummyRecordEasyScoreCalculator implements EasyScoreCalculato /* Dummy classes below are referenced from the testSolverConfig.xml used in this test case. */ - public static abstract class DummySolutionPartitioner implements SolutionPartitioner { + public abstract static class DummySolutionPartitioner implements SolutionPartitioner { } - public static abstract class DummyEasyScoreCalculator + public abstract static class DummyEasyScoreCalculator implements EasyScoreCalculator { } - public static abstract class DummyIncrementalScoreCalculator + public abstract static class DummyIncrementalScoreCalculator implements IncrementalScoreCalculator { } - public static abstract class DummyConstraintProvider implements ConstraintProvider { + public abstract static class DummyConstraintProvider implements ConstraintProvider { } public abstract static class DummyValueFilter implements SelectionFilter { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicyTestUtils.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicyTestUtils.java index f28fd0cac6..e43e367d5b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicyTestUtils.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicyTestUtils.java @@ -15,8 +15,11 @@ public static HeuristicConfigPolicy buildHeuristicConfigPolicy public static HeuristicConfigPolicy buildHeuristicConfigPolicy(SolutionDescriptor solutionDescriptor) { - return new HeuristicConfigPolicy.Builder<>(EnvironmentMode.REPRODUCIBLE, null, null, null, null, new Random(), null, - solutionDescriptor, ClassInstanceCache.create()) + return new HeuristicConfigPolicy.Builder() + .withEnvironmentMode(EnvironmentMode.REPRODUCIBLE) + .withRandom(new Random()) + .withSolutionDescriptor(solutionDescriptor) + .withClassInstanceCache(ClassInstanceCache.create()) .build(); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java index 663a493784..97663a54f0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java @@ -2,10 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.List; import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; import ai.timefold.solver.core.config.localsearch.decider.acceptor.AcceptorType; @@ -15,6 +19,7 @@ import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.greatdeluge.GreatDelugeAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; 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; @@ -57,7 +62,7 @@ void buildCompositeAcceptor() { .map(a -> (Class) a.getClass()) .containsExactly(HillClimbingAcceptor.class, StepCountingHillClimbingAcceptor.class, EntityTabuAcceptor.class, ValueTabuAcceptor.class, MoveTabuAcceptor.class, SimulatedAnnealingAcceptor.class, - LateAcceptanceAcceptor.class, GreatDelugeAcceptor.class); + LateAcceptanceAcceptor.class, DiversifiedLateAcceptanceAcceptor.class, GreatDelugeAcceptor.class); } @Test @@ -66,4 +71,44 @@ void noAcceptorConfigured_throwsException() { assertThatIllegalArgumentException().isThrownBy(() -> acceptorFactory.buildAcceptor(mock(HeuristicConfigPolicy.class))) .withMessageContaining("The acceptor does not specify any acceptorType"); } + + @Test + void lateAcceptanceAcceptor() { + var localSearchAcceptorConfig = new LocalSearchAcceptorConfig() + .withAcceptorTypeList(List.of(AcceptorType.LATE_ACCEPTANCE)); + HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); + AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); + var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + assertThat(acceptor).isExactlyInstanceOf(LateAcceptanceAcceptor.class); + + localSearchAcceptorConfig = new LocalSearchAcceptorConfig() + .withLateAcceptanceSize(10); + acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); + acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + assertThat(acceptor).isExactlyInstanceOf(LateAcceptanceAcceptor.class); + } + + @Test + void diversifiedLateAcceptanceAcceptor() { + var localSearchAcceptorConfig = new LocalSearchAcceptorConfig() + .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)); + HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); + AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); + var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + assertThat(acceptor).isExactlyInstanceOf(DiversifiedLateAcceptanceAcceptor.class); + + localSearchAcceptorConfig = new LocalSearchAcceptorConfig() + .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) + .withLateAcceptanceSize(10); + acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); + acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + assertThat(acceptor).isExactlyInstanceOf(DiversifiedLateAcceptanceAcceptor.class); + + doThrow(new IllegalStateException()).when(heuristicConfigPolicy).ensurePreviewFeature(any()); + localSearchAcceptorConfig = new LocalSearchAcceptorConfig() + .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) + .withLateAcceptanceSize(10); + AcceptorFactory badAcceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); + assertThatIllegalStateException().isThrownBy(() -> badAcceptorFactory.buildAcceptor(heuristicConfigPolicy)); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java new file mode 100644 index 0000000000..a30690e1c3 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -0,0 +1,145 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance; + +import static org.assertj.core.api.Assertions.assertThat; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +import org.junit.jupiter.api.Test; + +class DiversifiedLateAcceptanceAcceptorTest extends AbstractAcceptorTest { + + @Test + void acceptanceCriterion() { + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(); + acceptor.setLateAcceptanceSize(3); + + var solverScope = new SolverScope<>(); + solverScope.setBestScore(SimpleScore.of(-1000)); + var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); + acceptor.phaseStarted(phaseScope); + + // Equal to the current solution + var stepScope0 = new LocalSearchStepScope<>(phaseScope); + stepScope0.setScore(SimpleScore.of(-1000)); + var moveScope0 = buildMoveScope(stepScope0, -1000); + stepScope0.getPhaseScope().setLastCompletedStepScope(stepScope0); + assertThat(acceptor.isAccepted(moveScope0)).isTrue(); + + // Better than the current best late element + moveScope0 = buildMoveScope(stepScope0, -999); + assertThat(acceptor.isAccepted(moveScope0)).isTrue(); + + // Recompute lateWorse and the number of occurrences + acceptor.phaseStarted(phaseScope); + moveScope0 = buildMoveScope(stepScope0, -2000); + stepScope0.setScore(moveScope0.getScore()); + acceptor.lateWorse = SimpleScore.of(-2001); + acceptor.lateWorseOccurrences = 1; + acceptor.previousScores[0] = SimpleScore.of(-2001); + acceptor.previousScores[1] = SimpleScore.of(-2001); + acceptor.previousScores[2] = SimpleScore.of(-2000); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-2001)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2000)); + assertThat(acceptor.lateWorse).isEqualTo(SimpleScore.of(-2001)); + assertThat(acceptor.lateWorseOccurrences).isEqualTo(1); + } + + @Test + void replacementCriterion() { + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(); + acceptor.setLateAcceptanceSize(3); + + var solverScope = new SolverScope<>(); + solverScope.setBestScore(SimpleScore.of(-1000)); + var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); + acceptor.phaseStarted(phaseScope); + var stepScope0 = new LocalSearchStepScope<>(phaseScope); + stepScope0.setScore(SimpleScore.of(-1000)); + + // Current worse than late score and late score worse than previous + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + var moveScope0 = buildMoveScope(stepScope0, -2000); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-1999); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-1998)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2000)); + + // Current worse than late score and late score better than late score + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + moveScope0 = buildMoveScope(stepScope0, -2001); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-1999); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-2000)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2001)); + + // Current equal to previous and current worse than late score + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + moveScope0 = buildMoveScope(stepScope0, -2001); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-1999); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-2001)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2001)); + + // Current better than previous and previous better than late score + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + moveScope0 = buildMoveScope(stepScope0, -1998); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-2000); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-1999)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-1998)); + + // Current better than previous and previous worse than late score + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + moveScope0 = buildMoveScope(stepScope0, -1998); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-1999); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-2000)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-1998)); + + // Current worse than late score and previous worse than the current + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + moveScope0 = buildMoveScope(stepScope0, -2000); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-1999); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-2001)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2000)); + + // No replacement + // Current better than late score and previous better than current + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + moveScope0 = buildMoveScope(stepScope0, -2000); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-2001); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-1999)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2001)); + + // Current equal to previous and current better than late score + acceptor.phaseStarted(phaseScope); + acceptor.lateWorse = SimpleScore.of(-2005); + moveScope0 = buildMoveScope(stepScope0, -2000); + stepScope0.setScore(moveScope0.getScore()); + acceptor.previousScores[0] = SimpleScore.of(-2001); + stepScope0.getPhaseScope().getLastCompletedStepScope().setScore(SimpleScore.of(-2000)); + acceptor.isAccepted(moveScope0); + assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2001)); + } +} diff --git a/core/src/test/resources/ai/timefold/solver/core/config/solver/testSolverConfigWithEnumSet.xml b/core/src/test/resources/ai/timefold/solver/core/config/solver/testSolverConfigWithEnumSet.xml new file mode 100644 index 0000000000..cfaf16f65a --- /dev/null +++ b/core/src/test/resources/ai/timefold/solver/core/config/solver/testSolverConfigWithEnumSet.xml @@ -0,0 +1,18 @@ + + + DIVERSIFIED_LATE_ACCEPTANCE + FULL_ASSERT + AUTO + ai.timefold.solver.core.impl.testdata.domain.TestdataSolution + ai.timefold.solver.core.impl.testdata.domain.TestdataEntity + + ai.timefold.solver.core.config.solver.SolverConfigTest$DummyConstraintProvider + ONLY_DOWN + + + FIRST_FIT_DECREASING + + + TABU_SEARCH + + diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc index 4ae02939d2..06bcbba999 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc @@ -507,6 +507,64 @@ Use a lower tabu size than in a pure Tabu Search configuration. ---- +[#diversifiedLateAcceptance] +== Diversified Late acceptance + + +[#diversifiedLateAcceptanceAlgorithm] +=== Algorithm description + +Diversified Late Acceptance is similar to Late Acceptance, +but it offers different acceptance and replacement strategies. +A move is accepted if its score matches the current solution score +or is better than the late score (which is the winning score of a fixed number of steps ago). + +Diversified Late Acceptance was first proposed in +https://arxiv.org/pdf/1806.09328[Diversified Late Acceptance Search by M. Namazi, C. Sanderson, M. A. H. Newton, M. M. A. Polash, and A. Sattar] + +[#diversifiedLateAcceptanceConfiguration] +=== Configuration + +Simplest configuration: + +[source,xml,options="nowrap"] +---- + + DIVERSIFIED_LATE_ACCEPTANCE + ... + + DIVERSIFIED_LATE_ACCEPTANCE + + +---- + +The late elements list is updated as follows: + +* The current solution score is worse than the late score. +* The current solution score is better than the late score and different from the previous one. + +The size of the late elements list is typically smaller. +Advanced configuration: + +[source,xml,options="nowrap"] +---- + ... + + ... + + 5 + + + 1 + + +---- + +[IMPORTANT] +==== +The new acceptor is available as a xref:upgrading-timefold-solver/backwards-compatibility.adoc#previewFeatures[preview feature] +and must be specifically enabled with ``. +==== [#greatDeluge] == Great Deluge diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc index e3033d2ba6..8410363084 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc @@ -36,6 +36,7 @@ but we're just not entirely comfortable yet to write their signatures in stone. Timefold Solver includes several components which are only available as preview features. These are: +- _Diversified Late Acceptance_ acceptor in the package `ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance`. - _Move Streams API_ in the `ai.timefold.solver.core.api.move` package and its subpackages, as well as the `ai.timefold.solver.core.api.domain.metamodel` package and its subpackages. - _Timefold Solver for Python_, which is currently in beta. diff --git a/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json index e882fd9185..0a6bcda3b4 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json +++ b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json @@ -594,6 +594,10 @@ } ] }, + { + "name": "ai.timefold.solver.core.config.solver.PreviewFeature", + "allDeclaredFields": true + }, { "name": "ai.timefold.solver.core.config.solver.EnvironmentMode", "allDeclaredFields": true