diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index e0b9f8792e9..3a001e47cc1 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -51,6 +51,17 @@ "old": "method void ai.timefold.solver.core.api.solver.ProblemSizeStatistics::(long, long, long, double)", "new": "method void ai.timefold.solver.core.api.solver.ProblemSizeStatistics::(long, java.util.SequencedMap, java.lang.Long>, long, long, java.util.SequencedMap, java.util.SequencedMap>, double)", "justification": "Type is not supposed to be constructed by user; safe." + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.phase.PhaseConfig>", + "new": "class ai.timefold.solver.core.config.phase.PhaseConfig>", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"terminationConfig\"}", + "newValue": "{\"environmentMode\", \"terminationConfig\"}", + "justification": "Environment mode per phase" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/phase/PhaseConfig.java b/core/src/main/java/ai/timefold/solver/core/config/phase/PhaseConfig.java index f4d46e5b8cd..151b813b720 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/phase/PhaseConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/phase/PhaseConfig.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.partitionedsearch.PartitionedSearchPhaseConfig; import ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.config.util.ConfigUtils; @@ -24,6 +25,7 @@ PartitionedSearchPhaseConfig.class }) @XmlType(propOrder = { + "environmentMode", "terminationConfig" }) public abstract class PhaseConfig> extends AbstractConfig { @@ -31,6 +33,9 @@ public abstract class PhaseConfig> extends // 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 + // Per phase environment + protected EnvironmentMode environmentMode = null; + @XmlElement(name = "termination") protected TerminationConfig terminationConfig = null; @@ -38,6 +43,14 @@ public abstract class PhaseConfig> extends // Constructors and simple getters/setters // ************************************************************************ + public EnvironmentMode getEnvironmentMode() { + return environmentMode; + } + + public void setEnvironmentMode(EnvironmentMode environmentMode) { + this.environmentMode = environmentMode; + } + public @Nullable TerminationConfig getTerminationConfig() { return terminationConfig; } @@ -50,6 +63,11 @@ public void setTerminationConfig(@Nullable TerminationConfig terminationConfig) // With methods // ************************************************************************ + public @NonNull Config_ withEnvironmentMode(@NonNull EnvironmentMode environmentMode) { + this.setEnvironmentMode(environmentMode); + return (Config_) this; + } + public @NonNull Config_ withTerminationConfig(@NonNull TerminationConfig terminationConfig) { this.setTerminationConfig(terminationConfig); return (Config_) this; @@ -57,6 +75,7 @@ public void setTerminationConfig(@Nullable TerminationConfig terminationConfig) @Override public @NonNull Config_ inherit(@NonNull Config_ inheritedConfig) { + environmentMode = ConfigUtils.inheritOverwritableProperty(environmentMode, inheritedConfig.getEnvironmentMode()); terminationConfig = ConfigUtils.inheritConfig(terminationConfig, inheritedConfig.getTerminationConfig()); return (Config_) this; } diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 1434aaf5edf..68f7a14840e 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -196,8 +196,8 @@ LocalSearchDecider buildLocalSearch(int moveThreadCount, EnvironmentMode environmentMode, HeuristicConfigPolicy configPolicy); PartitionedSearchPhase buildPartitionedSearch(int phaseIndex, - PartitionedSearchPhaseConfig phaseConfig, HeuristicConfigPolicy solverConfigPolicy, - SolverTermination solverTermination, + PartitionedSearchPhaseConfig phaseConfig, EnvironmentMode environmentMode, + HeuristicConfigPolicy solverConfigPolicy, SolverTermination solverTermination, BiFunction, SolverTermination, PhaseTermination> phaseTerminationFunction); EntitySelector applyNearbySelection(EntitySelectorConfig entitySelectorConfig, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index cae734d261f..b947342442d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -191,11 +191,12 @@ public void phaseEnded(ConstructionHeuristicPhaseScope phaseScope) { if (decider.isLoggingEnabled() && logger.isInfoEnabled()) { logger.info( """ - {}Construction Heuristic phase ({}) ended: time spent ({}), best score ({}), \ + {}Construction Heuristic phase ({}) ended: time spent ({}), environment mode ({}), best score ({}), \ {}move evaluation speed ({}/sec), step total ({}).""", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), + environmentMode.name(), phaseScope.getBestScore().raw(), // Multithreaded solving uses "effective" move evaluation speed, since not all evaluated moves // are foraged @@ -232,17 +233,17 @@ public static class DefaultConstructionHeuristicPhaseBuilder private final EntityPlacer entityPlacer; private final ConstructionHeuristicDecider decider; - public DefaultConstructionHeuristicPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, String logIndentation, - PhaseTermination phaseTermination, EntityPlacer entityPlacer, - ConstructionHeuristicDecider decider) { - super(phaseIndex, lastInitializingPhase, logIndentation, phaseTermination); + public DefaultConstructionHeuristicPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, + EnvironmentMode environmentMode, String logIndentation, PhaseTermination phaseTermination, + EntityPlacer entityPlacer, ConstructionHeuristicDecider decider) { + super(phaseIndex, lastInitializingPhase, environmentMode, logIndentation, phaseTermination); this.entityPlacer = entityPlacer; this.decider = decider; } @Override - public DefaultConstructionHeuristicPhaseBuilder enableAssertions(EnvironmentMode environmentMode) { - super.enableAssertions(environmentMode); + public DefaultConstructionHeuristicPhaseBuilder enableAssertions() { + super.enableAssertions(); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java index 67aec712c0d..f81fd9e3648 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java @@ -17,6 +17,7 @@ import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService; import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.DefaultConstructionHeuristicPhaseBuilder; @@ -69,10 +70,11 @@ protected DefaultConstructionHeuristicPhaseBuilder createBuilder( HeuristicConfigPolicy phaseConfigPolicy, SolverTermination solverTermination, int phaseIndex, boolean lastInitializingPhase, EntityPlacer entityPlacer) { var phaseTermination = buildPhaseTermination(phaseConfigPolicy, solverTermination); - return new DefaultConstructionHeuristicPhaseBuilder<>(phaseIndex, lastInitializingPhase, + var environmentMode = resolveEnvironmentMode(phaseConfigPolicy); + return new DefaultConstructionHeuristicPhaseBuilder<>(phaseIndex, lastInitializingPhase, environmentMode, phaseConfigPolicy.getLogIndentation(), phaseTermination, entityPlacer, - buildDecider(phaseConfigPolicy, phaseTermination)) - .enableAssertions(phaseConfigPolicy.getEnvironmentMode()); + buildDecider(phaseConfigPolicy, environmentMode, phaseTermination)) + .enableAssertions(); } @Override @@ -158,14 +160,14 @@ public static EntityPlacerConfig buildListVariableQueuedValuePlacerConfig(Heuris } protected ConstructionHeuristicDecider buildDecider(HeuristicConfigPolicy configPolicy, - PhaseTermination termination) { + EnvironmentMode environmentMode, PhaseTermination termination) { var forager = buildForager(configPolicy); var moveThreadCount = configPolicy.getMoveThreadCount(); var decider = (moveThreadCount == null) ? new ConstructionHeuristicDecider<>(configPolicy.getLogIndentation(), termination, forager) : TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTITHREADED_SOLVING) .buildConstructionHeuristic(termination, forager, configPolicy); - decider.enableAssertions(configPolicy.getEnvironmentMode()); + decider.enableAssertions(environmentMode); return decider; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolder.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolder.java new file mode 100644 index 00000000000..a7ee3c0bff1 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolder.java @@ -0,0 +1,45 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import java.util.Objects; + +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; + +/** + * Demands a {@link ListVariableStateSupply} on {@link #phaseStarted(AbstractPhaseScope)} and releases it on + * {@link #phaseEnded(AbstractPhaseScope)}, re-demanding on every phase start because a phase may run under a + * different {@link ai.timefold.solver.core.config.solver.EnvironmentMode}, which swaps in a new score director + * (and thus a new {@link ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager}). + *

+ * Intended to be held as a field by selectors that need a {@link ListVariableStateSupply} across phase lifecycle + * events, delegating their own {@code phaseStarted}/{@code phaseEnded} overrides to this holder instead of each + * re-implementing the demand/cancel bookkeeping. + * + * @param the solution type, the class with the {@link ai.timefold.solver.core.api.domain.solution.PlanningSolution} + * annotation + */ +public final class ListVariableStateSupplyHolder { + + private final ListVariableDescriptor listVariableDescriptor; + private ListVariableStateSupply listVariableStateSupply; + + public ListVariableStateSupplyHolder(ListVariableDescriptor listVariableDescriptor) { + this.listVariableDescriptor = listVariableDescriptor; + } + + public void phaseStarted(AbstractPhaseScope phaseScope) { + listVariableStateSupply = phaseScope.getScoreDirector().getSupplyManager() + .demand(listVariableDescriptor.getStateDemand()); + } + + public void phaseEnded(AbstractPhaseScope phaseScope) { + phaseScope.getScoreDirector().getSupplyManager().cancel(listVariableDescriptor.getStateDemand()); + listVariableStateSupply = null; + } + + @SuppressWarnings("unchecked") + public ListVariableStateSupply get() { + return (ListVariableStateSupply) Objects.requireNonNull(listVariableStateSupply, + "Impossible state: The listVariableStateSupply is not initialized yet."); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java index 419903bcdbf..945bf0707e6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java @@ -101,11 +101,12 @@ private void phaseEnded(ExhaustiveSearchPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info(""" - {}Exhaustive Search phase ({}) ended: time spent ({}), best score ({}),\ + {}Exhaustive Search phase ({}) ended: time spent ({}), environment mode ({}), best score ({}),\ move evaluation speed ({}/sec), step total ({}).""", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), + environmentMode.name(), phaseScope.getBestScore().raw(), phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); @@ -141,17 +142,17 @@ public static class Builder extends AbstractPhaseBuilder { private boolean assertWorkingSolutionScoreFromScratch = false; private boolean assertExpectedWorkingSolutionScore = false; - public Builder(int phaseIndex, String logIndentation, PhaseTermination phaseTermination, - Comparator> nodeComparator, + public Builder(int phaseIndex, EnvironmentMode environmentMode, String logIndentation, + PhaseTermination phaseTermination, Comparator> nodeComparator, AbstractExhaustiveSearchDecider> decider) { - super(phaseIndex, logIndentation, phaseTermination); + super(phaseIndex, environmentMode, logIndentation, phaseTermination); this.nodeComparator = nodeComparator; this.decider = decider; } @Override - public Builder enableAssertions(EnvironmentMode environmentMode) { - super.enableAssertions(environmentMode); + public Builder enableAssertions() { + super.enableAssertions(); assertWorkingSolutionScoreFromScratch = environmentMode.isFullyAsserted(); assertExpectedWorkingSolutionScore = environmentMode.isIntrusivelyAsserted(); return this; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java index 9a44c060f06..d0269da2004 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java @@ -70,19 +70,20 @@ public ExhaustiveSearchPhase buildPhase(int phaseIndex, boolean lastI var phaseTermination = buildPhaseTermination(phaseConfigPolicy, solverTermination); var scoreBounderEnabled = exhaustiveSearchType.isScoreBounderEnabled(); var nodeExplorationType = getNodeExplorationType(exhaustiveSearchType, phaseConfig); + var environmentMode = resolveEnvironmentMode(phaseConfigPolicy); AbstractExhaustiveSearchDecider> decider; if (isMixedModel) { var basicVarEntitySelectorConfig = buildEntitySelectorConfig(phaseConfigPolicy, false); var basicVarEntitySelector = EntitySelectorFactory. create(basicVarEntitySelectorConfig) .buildEntitySelector(phaseConfigPolicy, SelectionCacheType.PHASE, SelectionOrder.ORIGINAL); var basicVarDecider = - buildDecider(phaseConfigPolicy, basicVarEntitySelector, bestSolutionRecaller, phaseTermination, - scoreBounderEnabled, false); + buildDecider(phaseConfigPolicy, basicVarEntitySelector, bestSolutionRecaller, environmentMode, + phaseTermination, scoreBounderEnabled, false); var listVarEntitySelectorConfig = buildEntitySelectorConfig(phaseConfigPolicy, true); var listVarEntitySelector = EntitySelectorFactory. create(listVarEntitySelectorConfig) .buildEntitySelector(phaseConfigPolicy, SelectionCacheType.PHASE, SelectionOrder.ORIGINAL); - var listVarDecider = buildDecider(phaseConfigPolicy, listVarEntitySelector, bestSolutionRecaller, phaseTermination, - scoreBounderEnabled, true); + var listVarDecider = buildDecider(phaseConfigPolicy, listVarEntitySelector, bestSolutionRecaller, environmentMode, + phaseTermination, scoreBounderEnabled, true); decider = new MixedVariableExhaustiveSearchDecider<>(basicVarDecider, listVarDecider); } else { var isListVariable = solverConfigPolicy.getSolutionDescriptor().getListVariableDescriptor() != null; @@ -90,12 +91,12 @@ public ExhaustiveSearchPhase buildPhase(int phaseIndex, boolean lastI var entitySelector = EntitySelectorFactory. create(entitySelectorConfig) .buildEntitySelector(phaseConfigPolicy, SelectionCacheType.PHASE, SelectionOrder.ORIGINAL); - decider = buildDecider(phaseConfigPolicy, entitySelector, bestSolutionRecaller, phaseTermination, + decider = buildDecider(phaseConfigPolicy, entitySelector, bestSolutionRecaller, environmentMode, phaseTermination, scoreBounderEnabled, isListVariable); } - return new DefaultExhaustiveSearchPhase.Builder<>(phaseIndex, solverConfigPolicy.getLogIndentation(), phaseTermination, - nodeExplorationType.buildNodeComparator(scoreBounderEnabled), decider) - .enableAssertions(phaseConfigPolicy.getEnvironmentMode()).build(); + return new DefaultExhaustiveSearchPhase.Builder<>(phaseIndex, environmentMode, solverConfigPolicy.getLogIndentation(), + phaseTermination, nodeExplorationType.buildNodeComparator(scoreBounderEnabled), decider) + .enableAssertions().build(); } private static NodeExplorationType getNodeExplorationType(ExhaustiveSearchType exhaustiveSearchType, @@ -158,8 +159,8 @@ protected EntityDescriptor deduceEntityDescriptor(SolutionDescriptor< private AbstractExhaustiveSearchDecider> buildDecider( HeuristicConfigPolicy configPolicy, EntitySelector sourceEntitySelector, - BestSolutionRecaller bestSolutionRecaller, PhaseTermination termination, - boolean scoreBounderEnabled, boolean isListVariable) { + BestSolutionRecaller bestSolutionRecaller, EnvironmentMode environmentMode, + PhaseTermination termination, boolean scoreBounderEnabled, boolean isListVariable) { var manualEntityMimicRecorder = new ManualEntityMimicRecorder<>(sourceEntitySelector); var entityClassName = sourceEntitySelector.getEntityDescriptor().getEntityClass().getName(); var mimicSelectorId = ConfigUtils.addRandomSuffix(entityClassName, configPolicy.getRandom().factoryUsage()); @@ -200,13 +201,7 @@ protected EntityDescriptor deduceEntityDescriptor(SolutionDescriptor< new MoveSelectorBasedMoveRepository<>(moveSelector), scoreBounderEnabled, scoreBounder); } - EnvironmentMode environmentMode = configPolicy.getEnvironmentMode(); - if (environmentMode.isFullyAsserted()) { - decider.setAssertMoveScoreFromScratch(true); - } - if (environmentMode.isIntrusivelyAsserted()) { - decider.setAssertExpectedUndoMoveScore(true); - } + decider.enableAssertions(environmentMode); return decider; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java index ac5bd431722..93e35bdbf8c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.exhaustivesearch.event.ExhaustiveSearchPhaseLifecycleListener; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchLayer; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchNode; @@ -57,19 +58,16 @@ public abstract sealed class AbstractExhaustiveSearchDecider getScoreBounder() { return (ScoreBounder) scoreBounder; } - public void setAssertMoveScoreFromScratch(boolean assertMoveScoreFromScratch) { - this.assertMoveScoreFromScratch = assertMoveScoreFromScratch; - } - - public void setAssertExpectedUndoMoveScore(boolean assertExpectedUndoMoveScore) { - this.assertExpectedUndoMoveScore = assertExpectedUndoMoveScore; - } - protected void enableAcceptUninitializedSolutions() { acceptUninitializedSolutions = true; } 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 a40baa67dbc..79f0d8ad4f2 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 @@ -130,15 +130,15 @@ public Builder cloneBuilder() { return new Builder() .withPreviewFeatureSet(previewFeatureSet) .withEnvironmentMode(environmentMode) + .withLogIndentation(logIndentation) .withMoveThreadCount(moveThreadCount) .withMoveThreadBufferSize(moveThreadBufferSize) .withThreadFactoryClass(threadFactoryClass) - .withNearbyDistanceMeterClass(nearbyDistanceMeterClass) - .withRandom(random) .withInitializingScoreTrend(initializingScoreTrend) .withSolutionDescriptor(solutionDescriptor) .withClassInstanceCache(classInstanceCache) - .withLogIndentation(logIndentation); + .withNearbyDistanceMeterClass(nearbyDistanceMeterClass) + .withRandom(random); } public HeuristicConfigPolicy copyConfigPolicy() { @@ -150,7 +150,7 @@ public HeuristicConfigPolicy copyConfigPolicy() { .build(); } - public HeuristicConfigPolicy createPhaseConfigPolicy() { + public HeuristicConfigPolicy copyPhaseConfigPolicy() { return cloneBuilder().build(); } @@ -160,7 +160,7 @@ public HeuristicConfigPolicy copyConfigPolicyWithoutNearbySetting() { .build(); } - public HeuristicConfigPolicy createChildThreadConfigPolicy(ChildThreadType childThreadType) { + public HeuristicConfigPolicy copyChildThreadConfigPolicy() { return cloneBuilder() .withLogIndentation(logIndentation + " ") .build(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java index 0037a9f9812..c55497912f0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java @@ -8,14 +8,14 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupplyHolder; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.ConcatenatingIterator; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.util.MappingIterator; import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; @@ -44,7 +44,7 @@ public class ElementDestinationSelector extends AbstractSelector listVariableStateSupply; + private final ListVariableStateSupplyHolder listVariableStateSupplyHolder; public ElementDestinationSelector(EntitySelector entitySelector, IterableValueSelector valueSelector, boolean randomSelection) { @@ -55,8 +55,9 @@ public ElementDestinationSelector(EntitySelector entitySelector, IterableValueSelector replayingValueSelector, IterableValueSelector valueSelector, boolean randomSelection, boolean isExhaustiveSearch) { this.listVariableDescriptor = (ListVariableDescriptor) valueSelector.getVariableDescriptor(); + this.listVariableStateSupplyHolder = new ListVariableStateSupplyHolder<>(listVariableDescriptor); this.entitySelector = entitySelector; - var selector = filterPinnedListPlanningVariableValuesWithIndex(valueSelector, this::getListVariableStateSupply); + var selector = filterPinnedListPlanningVariableValuesWithIndex(valueSelector, listVariableStateSupplyHolder::get); this.replayingValueSelector = replayingValueSelector; this.valueSelector = listVariableDescriptor.allowsUnassignedValues() ? filterUnassignedValues(selector) : selector; this.randomSelection = randomSelection; @@ -65,11 +66,6 @@ public ElementDestinationSelector(EntitySelector entitySelector, phaseLifecycleSupport.addEventListener(this.valueSelector); } - private ListVariableStateSupply getListVariableStateSupply() { - return Objects.requireNonNull(listVariableStateSupply, - "Impossible state: The listVariableStateSupply is not initialized yet."); - } - private IterableValueSelector filterUnassignedValues( IterableValueSelector valueSelector) { /* @@ -89,20 +85,21 @@ private IterableValueSelector filterUnassignedValues( * and always add one option to unassign at the end, * we can keep the correct probabilities throughout. */ - return FilteringValueSelector.ofAssigned(valueSelector, this::getListVariableStateSupply); + return FilteringValueSelector.ofAssigned(valueSelector, listVariableStateSupplyHolder::get); } @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - var supplyManager = solverScope.getScoreDirector().getSupplyManager(); - listVariableStateSupply = supplyManager.demand(listVariableDescriptor.getStateDemand()); + public void phaseStarted(AbstractPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + // The phase may operate in a different environment mode, which uses a new score director. + // We must ensure that the list variable state supply remains up to date. + listVariableStateSupplyHolder.phaseStarted(phaseScope); } @Override - public void solvingEnded(SolverScope solverScope) { - super.solvingEnded(solverScope); - listVariableStateSupply = null; + public void phaseEnded(AbstractPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + listVariableStateSupplyHolder.phaseEnded(phaseScope); } @Override @@ -124,9 +121,9 @@ public Iterator iterator() { // In case of list var which allows unassigned values, we need to exclude unassigned elements. var totalValueSize = valueSelector.getSize() - - (allowsUnassignedValues ? listVariableStateSupply.getUnassignedCount() : 0); + - (allowsUnassignedValues ? listVariableStateSupplyHolder.get().getUnassignedCount() : 0); var totalSize = Math.addExact(entitySelector.getSize(), totalValueSize); - return new ElementPositionRandomIterator<>(listVariableStateSupply, entitySelector, + return new ElementPositionRandomIterator<>(listVariableStateSupplyHolder.get(), entitySelector, replayingValueSelector != null ? replayingValueSelector.iterator() : null, valueSelector, workingRandom, totalSize, allowsUnassignedValues, allowsUnassignedValues && totalValueSize > 0); } else { @@ -146,7 +143,7 @@ public Iterator iterator() { // Value selector guarantees only unpinned values. var valueIterator = new MappingIterator<>(valueSelector.iterator(), v -> { - var pos = listVariableStateSupply.getElementPosition(v).ensureAssigned(); + var pos = listVariableStateSupplyHolder.get().getElementPosition(v).ensureAssigned(); return ElementPosition.of(pos.entity(), pos.index() + 1); }); if (listVariableDescriptor.allowsUnassignedValues()) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java index 05df568df39..3208490bf47 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java @@ -3,15 +3,14 @@ import static ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ListChangeMoveSelector.filterPinnedListPlanningVariableValuesWithIndex; import java.util.Iterator; -import java.util.Objects; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupplyHolder; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; public class RandomSubListSelector extends AbstractSelector implements SubListSelector { @@ -22,15 +21,16 @@ public class RandomSubListSelector extends AbstractSelector listVariableStateSupply; + private final ListVariableStateSupplyHolder listVariableStateSupplyHolder; public RandomSubListSelector( EntitySelector entitySelector, IterableValueSelector valueSelector, int minimumSubListSize, int maximumSubListSize) { this.entitySelector = entitySelector; - this.valueSelector = filterPinnedListPlanningVariableValuesWithIndex(valueSelector, this::getListVariableStateSupply); this.listVariableDescriptor = (ListVariableDescriptor) valueSelector.getVariableDescriptor(); + this.listVariableStateSupplyHolder = new ListVariableStateSupplyHolder<>(listVariableDescriptor); + this.valueSelector = filterPinnedListPlanningVariableValuesWithIndex(valueSelector, listVariableStateSupplyHolder::get); if (minimumSubListSize < 1) { throw new IllegalArgumentException("The minimumSubListSize (%d) must be greater than 0." .formatted(minimumSubListSize)); @@ -47,23 +47,20 @@ public RandomSubListSelector( phaseLifecycleSupport.addEventListener(this.valueSelector); } - private ListVariableStateSupply getListVariableStateSupply() { - return Objects.requireNonNull(listVariableStateSupply, - "Impossible state: The listVariableStateSupply is not initialized yet."); - } - @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - triangleElementFactory = new TriangleElementFactory(minimumSubListSize, maximumSubListSize, workingRandom); - var supplyManager = solverScope.getScoreDirector().getSupplyManager(); - listVariableStateSupply = supplyManager.demand(listVariableDescriptor.getStateDemand()); + public void phaseStarted(AbstractPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + this.triangleElementFactory = new TriangleElementFactory(minimumSubListSize, maximumSubListSize, workingRandom); + // The phase may run under a different environment mode, which swaps in a new score director + // (and thus a new SupplyManager); re-demand so the supply doesn't go stale. + listVariableStateSupplyHolder.phaseStarted(phaseScope); } @Override - public void solvingEnded(SolverScope solverScope) { - super.solvingEnded(solverScope); - listVariableStateSupply = null; + public void phaseEnded(AbstractPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + listVariableStateSupplyHolder.phaseEnded(phaseScope); + triangleElementFactory = null; } @Override @@ -143,7 +140,7 @@ protected SubList createUpcomingSelection() { // Using valueSelector instead of entitySelector is fairer // because entities with bigger list variables will be selected more often. var value = valueIterator.next(); - sourceEntity = listVariableStateSupply.getInverseSingleton(value); + sourceEntity = listVariableStateSupplyHolder.get().getInverseSingleton(value); if (sourceEntity == null) { // Ignore values which are unassigned. continue; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java index 09fdfca8711..51c9bb538cd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java @@ -55,7 +55,8 @@ public static RuinRecreateConstructionHeuristicPhaseBuilder constructionHeuristicPhaseFactory, PhaseTermination phaseTermination, EntityPlacer entityPlacer, ConstructionHeuristicDecider decider) { - super(0, false, "", phaseTermination, entityPlacer, decider); + // The R&R uses the root solver environment mode by default + super(0, false, configPolicy.getEnvironmentMode(), "", phaseTermination, entityPlacer, decider); this.configPolicy = configPolicy; this.constructionHeuristicPhaseFactory = constructionHeuristicPhaseFactory; this.phaseTermination = phaseTermination; @@ -73,7 +74,9 @@ public static RuinRecreateConstructionHeuristicPhaseBuilder(configPolicy, constructionHeuristicPhaseFactory, phaseTermination, super.getEntityPlacer().copy(), - constructionHeuristicPhaseFactory.buildDecider(configPolicy, phaseTermination)); + // The R&R decider uses the root solver environment mode by default + constructionHeuristicPhaseFactory.buildDecider(configPolicy, configPolicy.getEnvironmentMode(), + phaseTermination)); } return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseFactory.java index fe3d81a17f4..c96d0dbbc13 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseFactory.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhaseFactory; import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacer; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; @@ -20,14 +21,15 @@ protected RuinRecreateConstructionHeuristicPhaseBuilder createBuilder HeuristicConfigPolicy phaseConfigPolicy, SolverTermination solverTermination, int phaseIndex, boolean lastInitializingPhase, EntityPlacer entityPlacer) { var phaseTermination = PhaseTermination.bridge(new BasicPlumbingTermination(false)); + // The R&R decider uses the root solver environment mode by default return new RuinRecreateConstructionHeuristicPhaseBuilder<>(phaseConfigPolicy, this, phaseTermination, entityPlacer, - buildDecider(phaseConfigPolicy, phaseTermination)); + buildDecider(phaseConfigPolicy, phaseConfigPolicy.getEnvironmentMode(), phaseTermination)); } @Override protected RuinRecreateConstructionHeuristicDecider buildDecider(HeuristicConfigPolicy configPolicy, - PhaseTermination termination) { + EnvironmentMode environmentMode, PhaseTermination termination) { return new RuinRecreateConstructionHeuristicDecider<>(termination, buildForager(configPolicy)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelector.java index 02549522ab7..3ef0601edde 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelector.java @@ -1,16 +1,16 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list; import java.util.Iterator; -import java.util.Objects; import java.util.function.Supplier; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupplyHolder; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.list.DestinationSelector; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.GenericMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement; import ai.timefold.solver.core.preview.api.move.Move; @@ -20,29 +20,32 @@ public class ListChangeMoveSelector extends GenericMoveSelector destinationSelector; private final boolean randomSelection; - private ListVariableStateSupply listVariableStateSupply; + private final ListVariableStateSupplyHolder listVariableStateSupplyHolder; public ListChangeMoveSelector(IterableValueSelector sourceValueSelector, DestinationSelector destinationSelector, boolean randomSelection) { + var listVariableDescriptor = (ListVariableDescriptor) sourceValueSelector.getVariableDescriptor(); + this.listVariableStateSupplyHolder = new ListVariableStateSupplyHolder<>(listVariableDescriptor); this.sourceValueSelector = - filterPinnedListPlanningVariableValuesWithIndex(sourceValueSelector, this::getListVariableStateSupply); + filterPinnedListPlanningVariableValuesWithIndex(sourceValueSelector, listVariableStateSupplyHolder::get); this.destinationSelector = destinationSelector; this.randomSelection = randomSelection; phaseLifecycleSupport.addEventListener(this.sourceValueSelector); phaseLifecycleSupport.addEventListener(this.destinationSelector); } - private ListVariableStateSupply getListVariableStateSupply() { - return Objects.requireNonNull(listVariableStateSupply, - "Impossible state: The listVariableStateSupply is not initialized yet."); + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + // The phase may operate in a different environment mode, which uses a new score director. + // We must ensure that the list variable state supply remains up to date. + listVariableStateSupplyHolder.phaseStarted(phaseScope); } @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - var listVariableDescriptor = (ListVariableDescriptor) sourceValueSelector.getVariableDescriptor(); - var supplyManager = solverScope.getScoreDirector().getSupplyManager(); - this.listVariableStateSupply = supplyManager.demand(listVariableDescriptor.getStateDemand()); + public void phaseEnded(AbstractPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + listVariableStateSupplyHolder.phaseEnded(phaseScope); } public static IterableValueSelector filterPinnedListPlanningVariableValuesWithIndex( @@ -68,12 +71,6 @@ public static IterableValueSelector filterPinnedListPlann }); } - @Override - public void solvingEnded(SolverScope solverScope) { - super.solvingEnded(solverScope); - listVariableStateSupply = null; - } - @Override public long getSize() { return sourceValueSelector.getSize() * destinationSelector.getSize(); @@ -83,12 +80,12 @@ public long getSize() { public Iterator> iterator() { if (randomSelection) { return new RandomListChangeIterator<>( - listVariableStateSupply, + listVariableStateSupplyHolder.get(), sourceValueSelector, destinationSelector); } else { return new OriginalListChangeIterator<>( - listVariableStateSupply, + listVariableStateSupplyHolder.get(), sourceValueSelector, destinationSelector); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelector.java index d1413ca29d9..f6b66a388d6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelector.java @@ -3,13 +3,12 @@ import static ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ListChangeMoveSelector.filterPinnedListPlanningVariableValuesWithIndex; import java.util.Iterator; -import java.util.Objects; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupplyHolder; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.GenericMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.preview.api.move.Move; public class ListSwapMoveSelector extends GenericMoveSelector { @@ -18,45 +17,42 @@ public class ListSwapMoveSelector extends GenericMoveSelector rightValueSelector; private final boolean randomSelection; - private ListVariableStateSupply listVariableStateSupply; + private final ListVariableStateSupplyHolder listVariableStateSupplyHolder; public ListSwapMoveSelector(IterableValueSelector leftValueSelector, IterableValueSelector rightValueSelector, boolean randomSelection) { + var listVariableDescriptor = (ListVariableDescriptor) leftValueSelector.getVariableDescriptor(); + this.listVariableStateSupplyHolder = new ListVariableStateSupplyHolder<>(listVariableDescriptor); this.leftValueSelector = - filterPinnedListPlanningVariableValuesWithIndex(leftValueSelector, this::getListVariableStateSupply); + filterPinnedListPlanningVariableValuesWithIndex(leftValueSelector, listVariableStateSupplyHolder::get); this.rightValueSelector = - filterPinnedListPlanningVariableValuesWithIndex(rightValueSelector, this::getListVariableStateSupply); + filterPinnedListPlanningVariableValuesWithIndex(rightValueSelector, listVariableStateSupplyHolder::get); this.randomSelection = randomSelection; phaseLifecycleSupport.addEventListener(this.leftValueSelector); phaseLifecycleSupport.addEventListener(this.rightValueSelector); } - private ListVariableStateSupply getListVariableStateSupply() { - return Objects.requireNonNull(listVariableStateSupply, - "Impossible state: The listVariableStateSupply is not initialized yet."); - } - @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - var listVariableDescriptor = (ListVariableDescriptor) leftValueSelector.getVariableDescriptor(); - var supplyManager = solverScope.getScoreDirector().getSupplyManager(); - listVariableStateSupply = supplyManager.demand(listVariableDescriptor.getStateDemand()); + public void phaseStarted(AbstractPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + // The phase may operate in a different environment mode, which uses a new score director. + // We must ensure that the list variable state supply remains up to date. + listVariableStateSupplyHolder.phaseStarted(phaseScope); } @Override - public void solvingEnded(SolverScope solverScope) { - super.solvingEnded(solverScope); - listVariableStateSupply = null; + public void phaseEnded(AbstractPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + listVariableStateSupplyHolder.phaseEnded(phaseScope); } @Override public Iterator> iterator() { if (randomSelection) { - return new RandomListSwapIterator<>(listVariableStateSupply, leftValueSelector, rightValueSelector); + return new RandomListSwapIterator<>(listVariableStateSupplyHolder.get(), leftValueSelector, rightValueSelector); } else { - return new OriginalListSwapIterator<>(listVariableStateSupply, leftValueSelector, rightValueSelector); + return new OriginalListSwapIterator<>(listVariableStateSupplyHolder.get(), leftValueSelector, rightValueSelector); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveSelector.java index d01197fce94..002163338de 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveSelector.java @@ -3,15 +3,15 @@ import static ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ListChangeMoveSelector.filterPinnedListPlanningVariableValuesWithIndex; import java.util.Iterator; -import java.util.Objects; import java.util.function.Supplier; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupplyHolder; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.GenericMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.util.MathUtils; import ai.timefold.solver.core.preview.api.move.Move; @@ -26,14 +26,15 @@ final class KOptListMoveSelector extends GenericMoveSelector listVariableStateSupply; + private final ListVariableStateSupplyHolder listVariableStateSupplyHolder; public KOptListMoveSelector(ListVariableDescriptor listVariableDescriptor, IterableValueSelector originSelector, IterableValueSelector valueSelector, int minK, int maxK, int[] pickedKDistribution) { this.listVariableDescriptor = listVariableDescriptor; - this.originSelector = createEffectiveValueSelector(originSelector, this::getListVariableStateSupply); - this.valueSelector = createEffectiveValueSelector(valueSelector, this::getListVariableStateSupply); + this.listVariableStateSupplyHolder = new ListVariableStateSupplyHolder<>(listVariableDescriptor); + this.originSelector = createEffectiveValueSelector(originSelector, listVariableStateSupplyHolder::get); + this.valueSelector = createEffectiveValueSelector(valueSelector, listVariableStateSupplyHolder::get); this.minK = minK; this.maxK = maxK; this.pickedKDistribution = pickedKDistribution; @@ -50,22 +51,18 @@ private IterableValueSelector createEffectiveValueSelector( return FilteringValueSelector.ofAssigned(filteredValueSelector, listVariableStateSupplier); } - private ListVariableStateSupply getListVariableStateSupply() { - return Objects.requireNonNull(listVariableStateSupply, - "Impossible state: The listVariableStateSupply is not initialized yet."); - } - @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - var supplyManager = solverScope.getScoreDirector().getSupplyManager(); - listVariableStateSupply = supplyManager.demand(listVariableDescriptor.getStateDemand()); + public void phaseStarted(AbstractPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + // The phase may operate in a different environment mode, which uses a new score director. + // We must ensure that the list variable state supply remains up to date. + listVariableStateSupplyHolder.phaseStarted(phaseScope); } @Override - public void solvingEnded(SolverScope solverScope) { - super.solvingEnded(solverScope); - listVariableStateSupply = null; + public void phaseEnded(AbstractPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + listVariableStateSupplyHolder.phaseEnded(phaseScope); } @Override @@ -92,7 +89,7 @@ public long getSize() { @Override public Iterator> iterator() { - return new KOptListMoveIterator<>(workingRandom, listVariableDescriptor, listVariableStateSupply, + return new KOptListMoveIterator<>(workingRandom, listVariableDescriptor, listVariableStateSupplyHolder.get(), originSelector, valueSelector, minK, maxK, pickedKDistribution); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelector.java index 0de734404d4..cf4f818e438 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelector.java @@ -1,9 +1,8 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ruin; import java.util.Iterator; -import java.util.Objects; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupplyHolder; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.CountSupplier; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.GenericMoveSelector; @@ -18,21 +17,20 @@ final class ListRuinRecreateMoveSelector extends GenericMoveSelector { private final IterableValueSelector valueSelector; - private final ListVariableDescriptor listVariableDescriptor; private final RuinRecreateConstructionHeuristicPhaseBuilder constructionHeuristicPhaseBuilder; private final CountSupplier minimumSelectedCountSupplier; private final CountSupplier maximumSelectedCountSupplier; private SolverScope solverScope; - private ListVariableStateSupply listVariableStateSupply; + private final ListVariableStateSupplyHolder listVariableStateSupplyHolder; public ListRuinRecreateMoveSelector(IterableValueSelector valueSelector, ListVariableDescriptor listVariableDescriptor, RuinRecreateConstructionHeuristicPhaseBuilder constructionHeuristicPhaseBuilder, CountSupplier minimumSelectedCountSupplier, CountSupplier maximumSelectedCountSupplier) { super(); - this.valueSelector = FilteringValueSelector.ofAssigned(valueSelector, this::getListVariableStateSupply); - this.listVariableDescriptor = listVariableDescriptor; + this.listVariableStateSupplyHolder = new ListVariableStateSupplyHolder<>(listVariableDescriptor); + this.valueSelector = FilteringValueSelector.ofAssigned(valueSelector, listVariableStateSupplyHolder::get); this.constructionHeuristicPhaseBuilder = constructionHeuristicPhaseBuilder; this.minimumSelectedCountSupplier = minimumSelectedCountSupplier; this.maximumSelectedCountSupplier = maximumSelectedCountSupplier; @@ -40,11 +38,6 @@ public ListRuinRecreateMoveSelector(IterableValueSelector valueSelect phaseLifecycleSupport.addEventListener(this.valueSelector); } - private ListVariableStateSupply getListVariableStateSupply() { - return Objects.requireNonNull(listVariableStateSupply, - "Impossible state: The listVariableStateSupply is not initialized yet."); - } - @Override public long getSize() { var totalSize = 0L; @@ -64,23 +57,18 @@ public boolean isNeverEnding() { } @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - this.solverScope = solverScope; - this.listVariableStateSupply = solverScope.getScoreDirector() - .getSupplyManager() - .demand(listVariableDescriptor.getStateDemand()); - } - - @Override - public void solvingEnded(SolverScope solverScope) { - super.solvingEnded(solverScope); - this.listVariableStateSupply = null; + public void phaseStarted(AbstractPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + this.solverScope = phaseScope.getSolverScope(); + // The phase may operate in a different environment mode, which uses a new score director. + // We must ensure that the list variable state supply remains up to date. + listVariableStateSupplyHolder.phaseStarted(phaseScope); } @Override public void phaseEnded(AbstractPhaseScope phaseScope) { super.phaseEnded(phaseScope); + listVariableStateSupplyHolder.phaseEnded(phaseScope); this.solverScope = null; } @@ -88,7 +76,7 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { public Iterator> iterator() { var valueSelectorSize = valueSelector.getSize(); return new ListRuinRecreateMoveIterator<>(valueSelector, constructionHeuristicPhaseBuilder, - solverScope, listVariableStateSupply, + solverScope, listVariableStateSupplyHolder.get(), minimumSelectedCountSupplier.applyAsInt(valueSelectorSize), maximumSelectedCountSupplier.applyAsInt(valueSelectorSize), workingRandom); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index c3bfe0dbd95..330c53e4b60 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -228,11 +228,12 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info(""" - {}Local Search phase ({}) ended: time spent ({}), best score ({}), \ + {}Local Search phase ({}) ended: time spent ({}), environment mode ({}), best score ({}), \ {}move evaluation speed ({}/sec), step total ({}).""", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), + environmentMode.name(), phaseScope.getBestScore().raw(), // Multithreaded solving uses "effective" move evaluation speed, since not all evaluated moves // are foraged @@ -257,15 +258,15 @@ public static class Builder extends AbstractPhaseBuilder { private final LocalSearchDecider decider; - public Builder(int phaseIndex, String logIndentation, PhaseTermination phaseTermination, - LocalSearchDecider decider) { - super(phaseIndex, logIndentation, phaseTermination); + public Builder(int phaseIndex, EnvironmentMode environmentMode, String logIndentation, + PhaseTermination phaseTermination, LocalSearchDecider decider) { + super(phaseIndex, environmentMode, logIndentation, phaseTermination); this.decider = decider; } @Override - public Builder enableAssertions(EnvironmentMode environmentMode) { - super.enableAssertions(environmentMode); + public Builder enableAssertions() { + super.enableAssertions(); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 5b8c95be25a..8ee91baaa2c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -21,6 +21,7 @@ import ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig; import ai.timefold.solver.core.config.localsearch.decider.forager.LocalSearchForagerConfig; import ai.timefold.solver.core.config.localsearch.decider.forager.LocalSearchPickEarlyType; +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.enterprise.TimefoldSolverEnterpriseService; @@ -61,16 +62,17 @@ public DefaultLocalSearchPhaseFactory(LocalSearchPhaseConfig phaseConfig) { public LocalSearchPhase buildPhase(int phaseIndex, boolean lastInitializingPhase, HeuristicConfigPolicy solverConfigPolicy, BestSolutionRecaller bestSolutionRecaller, SolverTermination solverTermination) { - var phaseConfigPolicy = solverConfigPolicy.createPhaseConfigPolicy(); + var phaseConfigPolicy = solverConfigPolicy.copyPhaseConfigPolicy(); var phaseTermination = buildPhaseTermination(phaseConfigPolicy, solverTermination); - var decider = buildDecider(phaseConfigPolicy, phaseTermination); - return new DefaultLocalSearchPhase.Builder<>(phaseIndex, solverConfigPolicy.getLogIndentation(), phaseTermination, - decider).enableAssertions(phaseConfigPolicy.getEnvironmentMode()).build(); + var environmentMode = resolveEnvironmentMode(phaseConfigPolicy); + var decider = buildDecider(phaseConfigPolicy, environmentMode, phaseTermination); + return new DefaultLocalSearchPhase.Builder<>(phaseIndex, environmentMode, solverConfigPolicy.getLogIndentation(), + phaseTermination, decider).enableAssertions().build(); } @SuppressWarnings({ "unchecked", "rawtypes" }) private LocalSearchDecider buildDecider(HeuristicConfigPolicy phaseConfigPolicy, - PhaseTermination phaseTermination) { + EnvironmentMode environmentMode, PhaseTermination phaseTermination) { var neighborhoodsEnabled = phaseConfigPolicy.isPreviewFeatureEnabled(PreviewFeature.NEIGHBORHOODS); var neighborhoodProviderClass = phaseConfig. getNeighborhoodProviderClass(); if (neighborhoodsEnabled) { @@ -96,33 +98,34 @@ The neighborhoodProviderClass (%s) can only be used if the %s preview feature is var moveSelectorConfig = phaseConfig.getMoveSelectorConfig(); if (moveSelectorConfig != null) { if (neighborhoodsEnabled) { - return buildMixedDecider(phaseConfigPolicy, phaseTermination, neighborhoodProviderClass); + return buildMixedDecider(phaseConfigPolicy, environmentMode, phaseTermination, neighborhoodProviderClass); } else { - return buildMoveSelectorBasedDecider(phaseConfigPolicy, phaseTermination); + return buildMoveSelectorBasedDecider(phaseConfigPolicy, environmentMode, phaseTermination); } } else if (neighborhoodsEnabled) { - return buildNeighborhoodsBasedDecider(phaseConfigPolicy, phaseTermination, neighborhoodProviderClass); + return buildNeighborhoodsBasedDecider(phaseConfigPolicy, environmentMode, phaseTermination, + neighborhoodProviderClass); } else { // The default branch; for now, it is move selectors. - return buildMoveSelectorBasedDecider(phaseConfigPolicy, phaseTermination); + return buildMoveSelectorBasedDecider(phaseConfigPolicy, environmentMode, phaseTermination); } } private LocalSearchDecider buildMoveSelectorBasedDecider(HeuristicConfigPolicy configPolicy, - PhaseTermination termination) { + EnvironmentMode environmentMode, PhaseTermination termination) { var moveRepository = new MoveSelectorBasedMoveRepository<>(buildMoveSelector(configPolicy, false)); - return buildDecider(moveRepository, configPolicy, termination); + return buildDecider(moveRepository, configPolicy, environmentMode, termination); } private LocalSearchDecider buildNeighborhoodsBasedDecider(HeuristicConfigPolicy configPolicy, - PhaseTermination termination, + EnvironmentMode environmentMode, PhaseTermination termination, Class> neighborhoodProviderClass) { - return buildDecider(buildNeighborhoodsBasedMoveRepository(configPolicy, neighborhoodProviderClass), configPolicy, - termination); + return buildDecider(buildNeighborhoodsBasedMoveRepository(configPolicy, environmentMode, neighborhoodProviderClass), + configPolicy, environmentMode, termination); } @SuppressWarnings("unchecked") private NeighborhoodsBasedMoveRepository buildNeighborhoodsBasedMoveRepository( - HeuristicConfigPolicy configPolicy, + HeuristicConfigPolicy configPolicy, EnvironmentMode environmentMode, Class> neighborhoodProviderClass) { if (phaseConfig.getLocalSearchType() == LocalSearchType.VARIABLE_NEIGHBORHOOD_DESCENT) { throw new IllegalArgumentException( @@ -139,30 +142,33 @@ The localSearchType (%s) does not support the Neighborhoods API. "neighborhoodProviderClass", neighborhoodProviderClass); var solutionDescriptor = configPolicy.getSolutionDescriptor(); var neighborhoodBuilder = new DefaultNeighborhoodBuilder<>(solutionDescriptor.getMetaModel()); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, configPolicy.getEnvironmentMode()); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, environmentMode); return new NeighborhoodsBasedMoveRepository<>(moveStreamFactory, ((DefaultNeighborhood) neighborhoodProvider.defineNeighborhood(neighborhoodBuilder)) .getMoveProviderList()); } private LocalSearchDecider buildMixedDecider(HeuristicConfigPolicy configPolicy, - PhaseTermination termination, + EnvironmentMode environmentMode, PhaseTermination termination, Class> neighborhoodProviderClass) { var legacyMoveSelector = buildMoveSelector(configPolicy, neighborhoodProviderClass != null); if (legacyMoveSelector == null) { // There were no move selectors configured. - return buildNeighborhoodsBasedDecider(configPolicy, termination, neighborhoodProviderClass); + return buildNeighborhoodsBasedDecider(configPolicy, environmentMode, termination, neighborhoodProviderClass); } var neighborhoodsMoveSelector = - new NeighborhoodsMoveSelector<>(buildNeighborhoodsBasedMoveRepository(configPolicy, neighborhoodProviderClass)); + new NeighborhoodsMoveSelector<>( + buildNeighborhoodsBasedMoveRepository(configPolicy, environmentMode, neighborhoodProviderClass)); var moveSelector = new MixedMoveSelector<>(legacyMoveSelector, neighborhoodsMoveSelector); var moveRepository = new MoveSelectorBasedMoveRepository<>(moveSelector); - return buildDecider(moveRepository, configPolicy, termination); + return buildDecider(moveRepository, configPolicy, environmentMode, termination); } private LocalSearchDecider buildDecider(MoveRepository moveRepository, - HeuristicConfigPolicy configPolicy, PhaseTermination termination) { - var acceptor = buildAcceptor(configPolicy, moveRepository instanceof NeighborhoodsBasedMoveRepository); - var forager = buildForager(configPolicy); + HeuristicConfigPolicy configPolicy, EnvironmentMode environmentMode, + PhaseTermination termination) { + var acceptor = buildAcceptor(configPolicy, environmentMode, + moveRepository instanceof NeighborhoodsBasedMoveRepository); + var forager = buildForager(); if (moveRepository.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) { throw new IllegalStateException(""" The move repository (%s) is neverEnding (%s), but the forager (%s) does not support it. @@ -170,7 +176,6 @@ The move repository (%s) is neverEnding (%s), but the forager (%s) does not supp moveRepository.isNeverEnding(), forager)); } var moveThreadCount = configPolicy.getMoveThreadCount(); - var environmentMode = configPolicy.getEnvironmentMode(); var decider = moveThreadCount == null ? new LocalSearchDecider<>(configPolicy.getLogIndentation(), termination, moveRepository, acceptor, forager) : TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTITHREADED_SOLVING) @@ -180,7 +185,8 @@ The move repository (%s) is neverEnding (%s), but the forager (%s) does not supp return decider; } - protected Acceptor buildAcceptor(HeuristicConfigPolicy configPolicy, boolean neighborhoodsEnabled) { + protected Acceptor buildAcceptor(HeuristicConfigPolicy configPolicy, EnvironmentMode environmentMode, + boolean neighborhoodsEnabled) { var acceptorConfig = phaseConfig.getAcceptorConfig(); var localSearchType = phaseConfig.getLocalSearchType(); if (acceptorConfig != null) { @@ -189,23 +195,23 @@ protected Acceptor buildAcceptor(HeuristicConfigPolicy con "The localSearchType (%s) must not be configured if the acceptorConfig (%s) is explicitly configured." .formatted(localSearchType, acceptorConfig)); } - return buildAcceptor(acceptorConfig, configPolicy); + return buildAcceptor(acceptorConfig, configPolicy, environmentMode); } else { - var localSearchType_ = Objects.requireNonNullElse(localSearchType, LocalSearchType.LATE_ACCEPTANCE); - var acceptorConfig_ = new LocalSearchAcceptorConfig(); - if (neighborhoodsEnabled && localSearchType_ == LocalSearchType.VARIABLE_NEIGHBORHOOD_DESCENT) { + var updatedLocalSearchType = Objects.requireNonNullElse(localSearchType, LocalSearchType.LATE_ACCEPTANCE); + acceptorConfig = new LocalSearchAcceptorConfig(); + if (neighborhoodsEnabled && updatedLocalSearchType == LocalSearchType.VARIABLE_NEIGHBORHOOD_DESCENT) { // Maybe works, but never tested. throw new UnsupportedOperationException( "Variable Neighborhood descent is not yet supported with the Neighborhoods API."); } - var acceptorType = getAcceptorType(neighborhoodsEnabled, localSearchType_); - acceptorConfig_.setAcceptorTypeList(Collections.singletonList(acceptorType)); - return buildAcceptor(acceptorConfig_, configPolicy); + var acceptorType = getAcceptorType(neighborhoodsEnabled, updatedLocalSearchType); + acceptorConfig.setAcceptorTypeList(Collections.singletonList(acceptorType)); + return buildAcceptor(acceptorConfig, configPolicy, environmentMode); } } - private static @NonNull AcceptorType getAcceptorType(boolean neighborhoodsEnabled, LocalSearchType localSearchType_) { - var acceptorType = switch (localSearchType_) { + private static @NonNull AcceptorType getAcceptorType(boolean neighborhoodsEnabled, LocalSearchType localSearchType) { + var acceptorType = switch (localSearchType) { case HILL_CLIMBING, VARIABLE_NEIGHBORHOOD_DESCENT -> AcceptorType.HILL_CLIMBING; case TABU_SEARCH -> AcceptorType.ENTITY_TABU; case SIMULATED_ANNEALING -> AcceptorType.SIMULATED_ANNEALING; @@ -220,11 +226,11 @@ protected Acceptor buildAcceptor(HeuristicConfigPolicy con } private Acceptor buildAcceptor(LocalSearchAcceptorConfig acceptorConfig, - HeuristicConfigPolicy configPolicy) { - return AcceptorFactory. create(acceptorConfig).buildAcceptor(configPolicy); + HeuristicConfigPolicy configPolicy, EnvironmentMode environmentMode) { + return AcceptorFactory. create(acceptorConfig).buildAcceptor(configPolicy, environmentMode); } - protected LocalSearchForager buildForager(HeuristicConfigPolicy configPolicy) { + protected LocalSearchForager buildForager() { LocalSearchForagerConfig foragerConfig_; if (phaseConfig.getForagerConfig() != null) { if (phaseConfig.getLocalSearchType() != null) { @@ -245,10 +251,7 @@ protected LocalSearchForager buildForager(HeuristicConfigPolicy buildAcceptor(HeuristicConfigPolicy configPolicy) { + public Acceptor buildAcceptor(HeuristicConfigPolicy configPolicy, EnvironmentMode environmentMode) { List> acceptorList = Stream.of( buildHillClimbingAcceptor(), buildStepCountingHillClimbingAcceptor(), - buildEntityTabuAcceptor(configPolicy), - buildValueTabuAcceptor(configPolicy), - buildMoveTabuAcceptor(configPolicy), + buildEntityTabuAcceptor(environmentMode, configPolicy.getLogIndentation()), + buildValueTabuAcceptor(environmentMode, configPolicy.getLogIndentation()), + buildMoveTabuAcceptor(environmentMode, configPolicy.getLogIndentation()), buildSimulatedAnnealingAcceptor(configPolicy), buildLateAcceptanceAcceptor(), buildDiversifiedLateAcceptanceAcceptor(configPolicy), @@ -93,7 +94,8 @@ private Optional> buildStepCountingH return Optional.empty(); } - private Optional> buildEntityTabuAcceptor(HeuristicConfigPolicy configPolicy) { + private Optional> buildEntityTabuAcceptor(EnvironmentMode environmentMode, + String logIndentation) { var entityTabuSize = acceptorConfig.getEntityTabuSize(); var entityTabuRatio = acceptorConfig.getEntityTabuRatio(); var fadingEntityTabuSize = acceptorConfig.getFadingEntityTabuSize(); @@ -101,7 +103,7 @@ private Optional> buildEntityTabuAcceptor(Heuristi if (acceptorTypeListsContainsAcceptorType(AcceptorType.ENTITY_TABU) || entityTabuSize != null || entityTabuRatio != null || fadingEntityTabuSize != null || fadingEntityTabuRatio != null) { - var acceptor = new EntityTabuAcceptor(configPolicy.getLogIndentation()); + var acceptor = new EntityTabuAcceptor(logIndentation); if (entityTabuSize != null) { if (entityTabuRatio != null) { throw new IllegalArgumentException( @@ -124,15 +126,14 @@ private Optional> buildEntityTabuAcceptor(Heuristi } else if (fadingEntityTabuRatio != null) { acceptor.setFadingTabuSizeStrategy(new EntityRatioTabuSizeStrategy<>(fadingEntityTabuRatio)); } - if (configPolicy.getEnvironmentMode().isFullyAsserted()) { - acceptor.setAssertTabuHashCodeCorrectness(true); - } + acceptor.enableAssertions(environmentMode); return Optional.of(acceptor); } return Optional.empty(); } - private Optional> buildValueTabuAcceptor(HeuristicConfigPolicy configPolicy) { + private Optional> buildValueTabuAcceptor(EnvironmentMode environmentMode, + String logIndentation) { var valueTabuSize = acceptorConfig.getValueTabuSize(); var fadingValueTabuSize = acceptorConfig.getFadingValueTabuSize(); if (acceptorTypeListsContainsAcceptorType(AcceptorType.VALUE_TABU) @@ -142,27 +143,26 @@ private Optional> buildValueTabuAcceptor(HeuristicC "The acceptorType (%s) requires either valueTabuSize or fadingValueTabuSize to be configured." .formatted(AcceptorType.VALUE_TABU)); } - var acceptor = new ValueTabuAcceptor(configPolicy.getLogIndentation()); - configureFixedSizeTabuAcceptor(acceptor, configPolicy, valueTabuSize, fadingValueTabuSize); + var acceptor = new ValueTabuAcceptor(logIndentation); + configureFixedSizeTabuAcceptor(acceptor, environmentMode, valueTabuSize, fadingValueTabuSize); return Optional.of(acceptor); } return Optional.empty(); } private static void configureFixedSizeTabuAcceptor(AbstractTabuAcceptor acceptor, - HeuristicConfigPolicy configPolicy, Integer tabuSize, Integer fadingTabuSize) { + EnvironmentMode environmentMode, 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); - } + acceptor.enableAssertions(environmentMode); } - private Optional> buildMoveTabuAcceptor(HeuristicConfigPolicy configPolicy) { + private Optional> buildMoveTabuAcceptor(EnvironmentMode environmentMode, + String logIndentation) { var moveTabuSize = acceptorConfig.getMoveTabuSize(); var fadingMoveTabuSize = acceptorConfig.getFadingMoveTabuSize(); if (acceptorTypeListsContainsAcceptorType(AcceptorType.MOVE_TABU) @@ -172,8 +172,8 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon "The acceptorType (%s) requires either moveTabuSize or fadingMoveTabuSize to be configured." .formatted(AcceptorType.MOVE_TABU)); } - var acceptor = new MoveTabuAcceptor(configPolicy.getLogIndentation()); - configureFixedSizeTabuAcceptor(acceptor, configPolicy, moveTabuSize, fadingMoveTabuSize); + var acceptor = new MoveTabuAcceptor(logIndentation); + configureFixedSizeTabuAcceptor(acceptor, environmentMode, moveTabuSize, fadingMoveTabuSize); return Optional.of(acceptor); } return Optional.empty(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/AbstractTabuAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/AbstractTabuAcceptor.java index 91b782d2193..349abf48ad7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/AbstractTabuAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/AbstractTabuAcceptor.java @@ -4,7 +4,9 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.size.TabuSizeStrategy; @@ -54,8 +56,8 @@ public void setAspirationEnabled(boolean aspirationEnabled) { this.aspirationEnabled = aspirationEnabled; } - public void setAssertTabuHashCodeCorrectness(boolean assertTabuHashCodeCorrectness) { - this.assertTabuHashCodeCorrectness = assertTabuHashCodeCorrectness; + public void enableAssertions(EnvironmentMode environmentMode) { + assertTabuHashCodeCorrectness = environmentMode.isFullyAsserted(); } // ************************************************************************ @@ -100,7 +102,7 @@ protected void adjustTabuList(int tabuStepIndex, Collection<@Nullable Object> ta var oldTabuStepIndexInteger = tabuToStepIndexMap.get(oldTabu); if (oldTabuStepIndexInteger == null) { // oldTabu not null here, as null is a valid key and therefore has a valid corresponding value. - throw createHashcodeStabilityViolationException(oldTabu); + throw createHashcodeStabilityViolationException(Objects.requireNonNull(oldTabu)); } var oldTabuStepCount = tabuStepIndex - oldTabuStepIndexInteger; // at least 1 if (oldTabuStepCount < totalTabuListSize) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/DefaultPartitionedSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/DefaultPartitionedSearchPhaseFactory.java index 8037e578172..ac628e39a66 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/DefaultPartitionedSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/DefaultPartitionedSearchPhaseFactory.java @@ -18,8 +18,9 @@ public DefaultPartitionedSearchPhaseFactory(PartitionedSearchPhaseConfig phaseCo public PartitionedSearchPhase buildPhase(int phaseIndex, boolean lastInitializingPhase, HeuristicConfigPolicy solverConfigPolicy, BestSolutionRecaller bestSolutionRecaller, SolverTermination solverTermination) { + var environmentMode = resolveEnvironmentMode(solverConfigPolicy); return TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.PARTITIONED_SEARCH) - .buildPartitionedSearch(phaseIndex, phaseConfig, solverConfigPolicy, solverTermination, + .buildPartitionedSearch(phaseIndex, phaseConfig, environmentMode, solverConfigPolicy, solverTermination, this::buildPhaseTermination); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index b1d10f08e46..c99e39ed014 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -34,6 +34,7 @@ public abstract class AbstractPhase implements Phase { protected final transient Logger logger = LoggerFactory.getLogger(getClass()); protected final int phaseIndex; + protected final EnvironmentMode environmentMode; protected final String logIndentation; // Called "phaseTermination" to clearly distinguish from "solverTermination" inside AbstractSolver. @@ -49,6 +50,7 @@ public abstract class AbstractPhase implements Phase { protected AbstractPhase(AbstractPhaseBuilder builder) { phaseIndex = builder.phaseIndex; + environmentMode = builder.environmentMode; logIndentation = builder.logIndentation; phaseTermination = builder.phaseTermination; assertPhaseScoreFromScratch = builder.assertPhaseScoreFromScratch; @@ -83,6 +85,11 @@ public boolean isAssertShadowVariablesAreNotStaleAfterStep() { // Lifecycle methods // ************************************************************************ + @Override + public EnvironmentMode getEnvironmentMode() { + return environmentMode; + } + @Override public void solvingStarted(SolverScope solverScope) { phaseLifecycleSupport.fireSolvingStarted(solverScope); @@ -256,6 +263,7 @@ but planning list variable (%s) has (%d) unexpected unassigned values. public abstract static class AbstractPhaseBuilder { private final int phaseIndex; + protected final EnvironmentMode environmentMode; private final String logIndentation; private final PhaseTermination phaseTermination; @@ -264,13 +272,15 @@ public abstract static class AbstractPhaseBuilder { private boolean assertExpectedStepScore = false; private boolean assertShadowVariablesAreNotStaleAfterStep = false; - protected AbstractPhaseBuilder(int phaseIndex, String logIndentation, PhaseTermination phaseTermination) { + protected AbstractPhaseBuilder(int phaseIndex, EnvironmentMode environmentMode, String logIndentation, + PhaseTermination phaseTermination) { this.phaseIndex = phaseIndex; + this.environmentMode = environmentMode; this.logIndentation = logIndentation; this.phaseTermination = phaseTermination; } - public AbstractPhaseBuilder enableAssertions(EnvironmentMode environmentMode) { + public AbstractPhaseBuilder enableAssertions() { assertPhaseScoreFromScratch = environmentMode.isAsserted(); assertStepScoreFromScratch = environmentMode.isFullyAsserted(); assertExpectedStepScore = environmentMode.isIntrusivelyAsserted(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhaseFactory.java index 63cff4e91dc..53a9cae1011 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhaseFactory.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.config.partitionedsearch.PartitionedSearchPhaseConfig; import ai.timefold.solver.core.config.phase.PhaseConfig; import ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; import ai.timefold.solver.core.impl.exhaustivesearch.scope.ExhaustiveSearchPhaseScope; @@ -34,6 +35,10 @@ public AbstractPhaseFactory(PhaseConfig_ phaseConfig) { this.phaseConfig = phaseConfig; } + protected EnvironmentMode resolveEnvironmentMode(HeuristicConfigPolicy phaseConfigPolicy) { + return Objects.requireNonNullElse(phaseConfig.getEnvironmentMode(), phaseConfigPolicy.getEnvironmentMode()); + } + protected PhaseTermination buildPhaseTermination(HeuristicConfigPolicy configPolicy, SolverTermination solverTermination) { var terminationConfig_ = Objects.requireNonNullElseGet(phaseConfig.getTerminationConfig(), TerminationConfig::new); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPossiblyInitializingPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPossiblyInitializingPhase.java index cf3c7203e71..3c482f962cc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPossiblyInitializingPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPossiblyInitializingPhase.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.phase; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.phase.custom.CustomPhase; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; @@ -53,14 +54,14 @@ protected void ensureCorrectTermination(AbstractPhaseScope phaseScope } } - public static abstract class AbstractPossiblyInitializingPhaseBuilder + public abstract static class AbstractPossiblyInitializingPhaseBuilder extends AbstractPhaseBuilder { private final boolean lastInitializingPhase; - protected AbstractPossiblyInitializingPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, String phaseName, - PhaseTermination phaseTermination) { - super(phaseIndex, phaseName, phaseTermination); + protected AbstractPossiblyInitializingPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, + EnvironmentMode environmentMode, String phaseName, PhaseTermination phaseTermination) { + super(phaseIndex, environmentMode, phaseName, phaseTermination); this.lastInitializingPhase = lastInitializingPhase; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/Phase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/Phase.java index 8386fb516c3..2cd369b3a7f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/Phase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/Phase.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.event.EventProducerId; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -41,4 +42,5 @@ public interface Phase extends PhaseLifecycleListener { IntFunction getEventProducerIdSupplier(); + EnvironmentMode getEnvironmentMode(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java index 3496d7ac328..4f8093bb086 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java @@ -110,11 +110,12 @@ public void phaseEnded(CustomPhaseScope phaseScope) { super.phaseEnded(phaseScope); ensureCorrectTermination(phaseScope, logger); phaseScope.endingNow(); - logger.info("{}Custom phase ({}) ended: time spent ({}), best score ({})," + logger.info("{}Custom phase ({}) ended: time spent ({}), environment mode ({}), best score ({})," + " move evaluation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), + environmentMode.name(), phaseScope.getBestScore().raw(), phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); @@ -125,15 +126,16 @@ public static final class DefaultCustomPhaseBuilder private final List> customPhaseCommandList; - public DefaultCustomPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, String logIndentation, - PhaseTermination phaseTermination, List> customPhaseCommandList) { - super(phaseIndex, lastInitializingPhase, logIndentation, phaseTermination); + public DefaultCustomPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, EnvironmentMode environmentMode, + String logIndentation, PhaseTermination phaseTermination, + List> customPhaseCommandList) { + super(phaseIndex, lastInitializingPhase, environmentMode, logIndentation, phaseTermination); this.customPhaseCommandList = List.copyOf(customPhaseCommandList); } @Override - public DefaultCustomPhaseBuilder enableAssertions(EnvironmentMode environmentMode) { - super.enableAssertions(environmentMode); + public DefaultCustomPhaseBuilder enableAssertions() { + super.enableAssertions(); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhaseFactory.java index ab393e0ca25..2519c722438 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhaseFactory.java @@ -21,9 +21,10 @@ public DefaultCustomPhaseFactory(CustomPhaseConfig phaseConfig) { public CustomPhase buildPhase(int phaseIndex, boolean lastInitializingPhase, HeuristicConfigPolicy solverConfigPolicy, BestSolutionRecaller bestSolutionRecaller, SolverTermination solverTermination) { - var phaseConfigPolicy = solverConfigPolicy.createPhaseConfigPolicy(); + var phaseConfigPolicy = solverConfigPolicy.copyPhaseConfigPolicy(); var customPhaseCommandClassList = phaseConfig.getCustomPhaseCommandClassList(); var customPhaseCommandList = phaseConfig.getCustomPhaseCommandList(); + var environmentMode = resolveEnvironmentMode(phaseConfigPolicy); if (ConfigUtils.isEmptyCollection(customPhaseCommandClassList) && ConfigUtils.isEmptyCollection(customPhaseCommandList)) { throw new IllegalArgumentException( @@ -45,10 +46,10 @@ The customPhaseCommandClass (%s) cannot be null in the customPhase (%s). if (customPhaseCommandList != null) { customPhaseCommandList_.addAll((Collection) customPhaseCommandList); } - return new DefaultCustomPhase.DefaultCustomPhaseBuilder<>(phaseIndex, lastInitializingPhase, + return new DefaultCustomPhase.DefaultCustomPhaseBuilder<>(phaseIndex, lastInitializingPhase, environmentMode, solverConfigPolicy.getLogIndentation(), buildPhaseTermination(phaseConfigPolicy, solverTermination), customPhaseCommandList_) - .enableAssertions(phaseConfigPolicy.getEnvironmentMode()) + .enableAssertions() .build(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 6251f0001cc..20cfd85a34d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -63,6 +63,7 @@ public abstract class AbstractScoreDirector moveRepository; protected AbstractScoreDirector(AbstractScoreDirectorBuilder builder) { + this.environmentMode = builder.scoreDirectorFactory.getEnvironmentMode(); this.scoreDirectorFactory = builder.scoreDirectorFactory; // Needs early init, as supplies will need the instance to exist. this.neighborhoodsElementUpdateNotifier = new NeighborhoodNotifier<>(); @@ -111,7 +114,10 @@ protected AbstractScoreDirector(AbstractScoreDirectorBuilder(solutionDescriptor); this.shadowVariableSupport = ShadowVariableSupport.create(this); this.shadowVariableSupport.linkShadowVariables(); - this.solutionTracker = this.scoreDirectorFactory.isTrackingWorkingSolution() + // When true, a snapshot of the solution is created before, after and after the undo of a move. + // In {@link EnvironmentMode#TRACKED_FULL_ASSERT}, the snapshots are compared when corruption is detected, + // allowing us to report exactly what variables are different. + this.solutionTracker = environmentMode.isTracking() ? new SolutionTracker<>(getSolutionDescriptor(), getSupplyManager()) : null; this.valueRangeManager = new ValueRangeManager<>(solutionDescriptor); @@ -122,8 +128,9 @@ protected AbstractScoreDirector(AbstractScoreDirectorBuilder createChildThreadScoreDirector(ChildThreadType childThreadType) { // Most score directors don't need derived status; CS will override this. - if (childThreadType == ChildThreadType.PART_THREAD) { - var childThreadScoreDirector = scoreDirectorFactory.createScoreDirectorBuilder().withLookUpEnabled(lookUpEnabled) - .withConstraintMatchPolicy(constraintMatchPolicy).buildDerived(); - // ScoreCalculationCountTermination takes into account previous phases - // but the calculationCount of partitions is maxed, not summed. - childThreadScoreDirector.calculationCount = calculationCount; - return childThreadScoreDirector; - } else if (childThreadType == ChildThreadType.MOVE_THREAD) { - var childThreadScoreDirector = scoreDirectorFactory.createScoreDirectorBuilder().withLookUpEnabled(true) - .withConstraintMatchPolicy(constraintMatchPolicy).buildDerived(); - childThreadScoreDirector.setWorkingSolution(cloneWorkingSolution()); - return childThreadScoreDirector; - } else { - throw new IllegalStateException("The childThreadType (" + childThreadType + ") is not implemented."); + switch (childThreadType) { + case PART_THREAD -> { + var childThreadScoreDirector = + scoreDirectorFactory.createScoreDirectorBuilder().withLookUpEnabled(lookUpEnabled) + .withConstraintMatchPolicy(constraintMatchPolicy).buildDerived(); + // ScoreCalculationCountTermination takes into account previous phases + // but the calculationCount of partitions is maxed, not summed. + childThreadScoreDirector.calculationCount = calculationCount; + return childThreadScoreDirector; + } + case MOVE_THREAD -> { + var childThreadScoreDirector = scoreDirectorFactory.createScoreDirectorBuilder().withLookUpEnabled(true) + .withConstraintMatchPolicy(constraintMatchPolicy).buildDerived(); + childThreadScoreDirector.setWorkingSolution(cloneWorkingSolution()); + return childThreadScoreDirector; + } + default -> throw new IllegalStateException("The childThreadType (" + childThreadType + ") is not implemented."); } } @@ -665,6 +675,32 @@ public void afterProblemFactRemoved(Object problemFact) { // Assert methods // ************************************************************************ + /** + * Asserts that if the {@link Score} is calculated for the parameter solution, + * it would be equal to the score of that parameter. + * + * @param solution never null + * @see InnerScoreDirector#assertWorkingScoreFromScratch(InnerScore, Object) + */ + @Override + public void assertScoreFromScratch(Solution_ solution) { + // Get the score before uncorruptedScoreDirector.calculateScore() modifies it + var score = getSolutionDescriptor(). getScore(solution); + // Most score directors don't need derived status; CS will override this. + try (var uncorruptedScoreDirector = scoreDirectorFactory.createScoreDirectorBuilder() + .withConstraintMatchPolicy(ConstraintMatchPolicy.ENABLED) + .buildDerived()) { + uncorruptedScoreDirector.setWorkingSolution(solution); + var uncorruptedScore = uncorruptedScoreDirector.calculateScore() + .raw(); + if (!score.equals(uncorruptedScore)) { + throw new IllegalStateException( + "Score corruption (%s): the solution's score (%s) is not the uncorruptedScore (%s)." + .formatted(score.subtract(uncorruptedScore).toShortString(), score, uncorruptedScore)); + } + } + } + @Override public void assertExpectedWorkingScore(InnerScore expectedWorkingScore, Object completedAction) { var workingScore = calculateScore(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java index 9de1add6391..971ccef092b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.director; +import java.util.Objects; + import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -7,7 +9,6 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; @@ -34,15 +35,17 @@ public abstract class AbstractScoreDirectorFactory assertionScoreDirectorFactory = null; - protected boolean assertClonedSolution = false; - protected boolean trackingWorkingSolution = false; - - public AbstractScoreDirectorFactory(SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { + protected AbstractScoreDirectorFactory(SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { this.solutionDescriptor = solutionDescriptor; - this.environmentMode = environmentMode; + this.environmentMode = Objects.requireNonNull(environmentMode); this.listVariableDescriptor = solutionDescriptor.getListVariableDescriptor(); } + @Override + public EnvironmentMode getEnvironmentMode() { + return environmentMode; + } + @Override public SolutionDescriptor getSolutionDescriptor() { return solutionDescriptor; @@ -70,47 +73,6 @@ public void setAssertionScoreDirectorFactory(ScoreDirectorFactory getScore(solution); - // Most score directors don't need derived status; CS will override this. - try (var uncorruptedScoreDirector = createScoreDirectorBuilder() - .withConstraintMatchPolicy(ConstraintMatchPolicy.ENABLED) - .buildDerived()) { - uncorruptedScoreDirector.setWorkingSolution(solution); - var uncorruptedScore = uncorruptedScoreDirector.calculateScore() - .raw(); - if (!score.equals(uncorruptedScore)) { - throw new IllegalStateException( - "Score corruption (%s): the solution's score (%s) is not the uncorruptedScore (%s)." - .formatted(score.subtract(uncorruptedScore).toShortString(), score, uncorruptedScore)); - } - } - } - public EntityDescriptor validateEntity(ScoreDirector scoreDirector, Object entity) { if (listVariableDescriptor == null) { // Only basic variables. var entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(entity.getClass()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactory.java similarity index 75% rename from core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java rename to core/src/main/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactory.java index 08f3c3aa547..0870b7d1aa6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactory.java @@ -7,19 +7,43 @@ import ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.incremental.IncrementalScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; -public class ScoreDirectorFactoryFactory> { +import org.jspecify.annotations.NullMarked; + +/** + * The delegate score factory creates a {@link ScoreDirectorFactory} based on the specified environment mode. + * This functionality enables the creation of different score director factories using the delegate. + * It is necessary because the solver phases may operate under various environment modes, + * requiring the creation of different factories. + */ +@NullMarked +public class DelegateScoreDirectorFactory> { private final ScoreDirectorFactoryConfig config; + private final boolean hasMetricRequiringConstraintMatch; + + public DelegateScoreDirectorFactory(ScoreDirectorFactoryConfig config) { + this(config, false); + } - public ScoreDirectorFactoryFactory(ScoreDirectorFactoryConfig config) { + public DelegateScoreDirectorFactory(ScoreDirectorFactoryConfig config, boolean hasMetricRequiringConstraintMatch) { this.config = config; + this.hasMetricRequiringConstraintMatch = hasMetricRequiringConstraintMatch; + assertCorrectDirectorFactory(config); } + /** + * Build a score director factory according to the given environment mode. + * + * @param environmentMode the environment mode + * @param solutionDescriptor the solution descriptor + * @return a new instance of the score director factory compatible with the environment mode and solver configuration. + */ public ScoreDirectorFactory buildScoreDirectorFactory(EnvironmentMode environmentMode, SolutionDescriptor solutionDescriptor) { var scoreDirectorFactory = decideMultipleScoreDirectorFactories(solutionDescriptor, environmentMode); @@ -37,7 +61,7 @@ public ScoreDirectorFactory buildScoreDirectorFactory(Environ .formatted(assertionScoreDirectorFactory, environmentMode, EnvironmentMode.STEP_ASSERT)); } var assertionScoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(assertionScoreDirectorFactory); + new DelegateScoreDirectorFactory(assertionScoreDirectorFactory); scoreDirectorFactory.setAssertionScoreDirectorFactory(assertionScoreDirectorFactoryFactory .buildScoreDirectorFactory(EnvironmentMode.NON_REPRODUCIBLE, solutionDescriptor)); } @@ -45,19 +69,27 @@ public ScoreDirectorFactory buildScoreDirectorFactory(Environ config.getInitializingScoreTrend() == null ? InitializingScoreTrendLevel.ANY.name() : config.getInitializingScoreTrend(), solutionDescriptor.getScoreDefinition().getLevelsSize())); - if (environmentMode.isFullyAsserted()) { - scoreDirectorFactory.setAssertClonedSolution(true); - } - if (environmentMode.isTracking()) { - scoreDirectorFactory.setTrackingWorkingSolution(true); - } return scoreDirectorFactory; } - protected AbstractScoreDirectorFactory decideMultipleScoreDirectorFactories( - SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { - assertCorrectDirectorFactory(config); + /** + * Creates a new instance of the score director. + * + * @param scoreScoreDirectorFactory the factory to be used to create the score director instance. + */ + public InnerScoreDirector + createScoreDirector(ScoreDirectorFactory scoreScoreDirectorFactory) { + var isConstraintMatchEnabled = + hasMetricRequiringConstraintMatch || scoreScoreDirectorFactory.getEnvironmentMode().isStepAssertOrMore(); + return scoreScoreDirectorFactory.createScoreDirectorBuilder() + .withLookUpEnabled(true) // Custom phases and problem changes may rely on lookups. + .withConstraintMatchPolicy( + isConstraintMatchEnabled ? ConstraintMatchPolicy.ENABLED : ConstraintMatchPolicy.DISABLED) + .build(); + } + private AbstractScoreDirectorFactory decideMultipleScoreDirectorFactories( + SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { // At this point, we are guaranteed to have at most one score director factory selected. if (config.getEasyScoreCalculatorClass() != null) { return EasyScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 6f3caa961e9..1a318fd29af 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -209,7 +209,11 @@ default Solution_ cloneWorkingSolution() { void resetCalculationCount(); - void incrementCalculationCount(); + default void incrementCalculationCount() { + incrementCalculationCount(1L); + } + + void incrementCalculationCount(long count); /** * @return never null @@ -225,6 +229,15 @@ default Solution_ cloneWorkingSolution() { InnerScoreDirector createChildThreadScoreDirector(ChildThreadType childThreadType); + /** + * Asserts that if the {@link Score} is calculated for the parameter solution, + * it would be equal to the score of that parameter. + * + * @param solution never null + * @see InnerScoreDirector#assertWorkingScoreFromScratch(InnerScore, Object) + */ + void assertScoreFromScratch(Solution_ solution); + /** * Do not waste performance by propagating changes to step (or higher) mechanisms. * @@ -274,7 +287,6 @@ default Solution_ cloneWorkingSolution() { * @param workingScore never null * @param completedAction sometimes null, when assertion fails then the completedAction's {@link Object#toString()} * is included in the exception message - * @see ScoreDirectorFactory#assertScoreFromScratch */ void assertWorkingScoreFromScratch(InnerScore workingScore, Object completedAction); @@ -288,7 +300,6 @@ default Solution_ cloneWorkingSolution() { * @param predictedScore never null * @param completedAction sometimes null, when assertion fails then the completedAction's {@link Object#toString()} * is included in the exception message - * @see ScoreDirectorFactory#assertScoreFromScratch */ void assertPredictedScoreFromScratch(InnerScore predictedScore, Object completedAction); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java index b73ed455239..c1195d4b90e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java @@ -2,8 +2,10 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector.AbstractScoreDirectorBuilder; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; /** @@ -22,24 +24,24 @@ public interface ScoreDirectorFactory> { */ ScoreDefinition getScoreDefinition(); - AbstractScoreDirector.AbstractScoreDirectorBuilder createScoreDirectorBuilder(); + , Builder_ extends AbstractScoreDirectorBuilder> + AbstractScoreDirectorBuilder + createScoreDirectorBuilder(); - default AbstractScoreDirector buildScoreDirector() { - return createScoreDirectorBuilder().build(); + default > + AbstractScoreDirector buildScoreDirector() { + AbstractScoreDirectorBuilder builder = createScoreDirectorBuilder(); + return builder.build(); } /** * @return never null */ - InitializingScoreTrend getInitializingScoreTrend(); + EnvironmentMode getEnvironmentMode(); /** - * Asserts that if the {@link Score} is calculated for the parameter solution, - * it would be equal to the score of that parameter. - * - * @param solution never null - * @see InnerScoreDirector#assertWorkingScoreFromScratch(InnerScore, Object) + * @return never null */ - void assertScoreFromScratch(Solution_ solution); + InitializingScoreTrend getInitializingScoreTrend(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java index d6dd5f496d1..776bcd9fd84 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java @@ -6,13 +6,19 @@ import java.util.random.RandomGenerator; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.event.SolverEventListener; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.phase.Phase; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.DelegateScoreDirectorFactory; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; +import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; import ai.timefold.solver.core.impl.solver.event.SolverEventSupport; import ai.timefold.solver.core.impl.solver.random.DefaultRandomSource; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; @@ -39,6 +45,8 @@ public abstract class AbstractSolver implements Solver { protected final transient Logger LOGGER = LoggerFactory.getLogger(getClass()); + protected final SolverContext defaultSolverContext; + private final DelegateScoreDirectorFactory delegateScoreDirectorFactory; private final SolverEventSupport solverEventSupport = new SolverEventSupport<>(this); private final PhaseLifecycleSupport phaseLifecycleSupport = new PhaseLifecycleSupport<>(); @@ -49,17 +57,23 @@ public abstract class AbstractSolver implements Solver { protected final List> phaseList; private RandomGenerator.@Nullable SplittableGenerator savedRandom; + private SolverContext currentContext; // ************************************************************************ // Constructors and simple getters/setters // ************************************************************************ - protected AbstractSolver(BestSolutionRecaller bestSolutionRecaller, - UniversalTermination globalTermination, List> phaseList) { + protected AbstractSolver(SolverContext defaultSolverContext, + DelegateScoreDirectorFactory delegateScoreDirectorFactory, + BestSolutionRecaller bestSolutionRecaller, UniversalTermination globalTermination, + List> phaseList) { + this.delegateScoreDirectorFactory = delegateScoreDirectorFactory; this.bestSolutionRecaller = bestSolutionRecaller; this.globalTermination = globalTermination; bestSolutionRecaller.setSolverEventSupport(solverEventSupport); this.phaseList = List.copyOf(phaseList); + this.defaultSolverContext = defaultSolverContext; + this.currentContext = defaultSolverContext; } public void solvingStarted(SolverScope solverScope) { @@ -86,6 +100,7 @@ protected void runPhases(SolverScope solverScope) { Iterator> it = phaseList.iterator(); while (!globalTermination.isSolverTerminated(solverScope) && it.hasNext()) { Phase phase = it.next(); + preparePhase(solverScope, phase); phase.solve(solverScope); // If there is a next phase, it starts from the best solution, which might differ from the working solution. // If there isn't, no need to planning clone the best solution to the working solution. @@ -95,6 +110,45 @@ protected void runPhases(SolverScope solverScope) { } } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void preparePhase(SolverScope solverScope, Phase phase) { + // The environment modes match, and there is no need for any changes. + if (phase.getEnvironmentMode() == currentContext.environmentMode()) { + return; + } + // The phase environment mode matches default, so we will restore it. + if (phase.getEnvironmentMode() == defaultSolverContext.environmentMode()) { + // Update and load the default context + loadContext(currentContext, defaultSolverContext, solverScope); + return; + } + // Since the current logic does not cache any solver context other than the default, + // we need to create a new solver context + // because the required environment mode differs from both the current and the default modes. + ScoreDirectorFactory newScoreDirectorFactory = delegateScoreDirectorFactory + .buildScoreDirectorFactory(phase.getEnvironmentMode(), solverScope.getSolutionDescriptor()); + var newScoreDirector = delegateScoreDirectorFactory.createScoreDirector(newScoreDirectorFactory); + var newSolverContext = new SolverContext<>(phase.getEnvironmentMode(), newScoreDirector, + new DefaultProblemChangeDirector<>(newScoreDirector)); + loadContext(currentContext, newSolverContext, solverScope); + } + + private void loadContext(SolverContext oldSolverContext, SolverContext newSolverContext, + SolverScope solverScope) { + solverScope.setScoreDirector(newSolverContext.scoreDirector()); + solverScope.setProblemChangeDirector(newSolverContext.problemChangeDirector()); + // We will use the same working solution set from the previous phase, as it has already been cloned + newSolverContext.scoreDirector().setWorkingSolution(oldSolverContext.scoreDirector().getWorkingSolution()); + bestSolutionRecaller.enableAssertions(newSolverContext.environmentMode()); + // Ensure that the score calculation count is consistent for the new director + newSolverContext.scoreDirector().resetCalculationCount(); + newSolverContext.scoreDirector().incrementCalculationCount(oldSolverContext.scoreDirector().getCalculationCount()); + if (oldSolverContext != defaultSolverContext) { + oldSolverContext.release(); + } + currentContext = newSolverContext; + } + public void solvingEnded(SolverScope solverScope) { for (Phase phase : phaseList) { phase.solvingEnded(solverScope); @@ -102,6 +156,11 @@ public void solvingEnded(SolverScope solverScope) { bestSolutionRecaller.solvingEnded(solverScope); globalTermination.solvingEnded(solverScope); phaseLifecycleSupport.fireSolvingEnded(solverScope); + if (currentContext != defaultSolverContext) { + // Restore the default context + // so solverScope operate on the original score director + loadContext(currentContext, defaultSolverContext, solverScope); + } } public void solvingError(SolverScope solverScope, Exception exception) { @@ -109,6 +168,11 @@ public void solvingError(SolverScope solverScope, Exception exception for (Phase phase : phaseList) { phase.solvingError(solverScope, exception); } + if (currentContext != defaultSolverContext) { + // A phase may have failed while operating under a non-default environment mode, + // and we need to restore the default context + loadContext(currentContext, defaultSolverContext, solverScope); + } } public void phaseStarted(AbstractPhaseScope phaseScope) { @@ -183,8 +247,27 @@ public BestSolutionRecaller getBestSolutionRecaller() { return bestSolutionRecaller; } + @SuppressWarnings("unchecked") + public > DelegateScoreDirectorFactory getDelegateScoreDirectorFactory() { + return (DelegateScoreDirectorFactory) delegateScoreDirectorFactory; + } + public List> getPhaseList() { return phaseList; } + public record SolverContext>(EnvironmentMode environmentMode, + InnerScoreDirector scoreDirector, + DefaultProblemChangeDirector problemChangeDirector) { + + public static > SolverContext + of(EnvironmentMode environmentMode, SolverScope solverScope) { + return new SolverContext<>(environmentMode, solverScope. getScoreDirector(), + solverScope.getProblemChangeDirector()); + } + + void release() { + scoreDirector.close(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index a9372f59000..d7f4a81dc47 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.domain.common.PlanningId; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.change.ProblemChange; import ai.timefold.solver.core.api.solver.event.EventProducerId; @@ -16,6 +17,7 @@ import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.phase.Phase; +import ai.timefold.solver.core.impl.score.director.DelegateScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; import ai.timefold.solver.core.impl.solver.random.RandomSource; @@ -39,23 +41,23 @@ @NullMarked public class DefaultSolver extends AbstractSolver { - protected final EnvironmentMode environmentMode; - protected final Supplier randomFactory; - protected final BasicPlumbingTermination basicPlumbingTermination; - protected final AtomicBoolean solving = new AtomicBoolean(false); - protected final SolverScope solverScope; + private final Supplier randomFactory; + private final BasicPlumbingTermination basicPlumbingTermination; + private final AtomicBoolean solving = new AtomicBoolean(false); + private final SolverScope solverScope; private final String moveThreadCountDescription; // ************************************************************************ // Constructors and simple getters/setters // ************************************************************************ - public DefaultSolver(EnvironmentMode environmentMode, Supplier randomFactory, - BestSolutionRecaller bestSolutionRecaller, BasicPlumbingTermination basicPlumbingTermination, - UniversalTermination termination, List> phaseList, - SolverScope solverScope, String moveThreadCountDescription) { - super(bestSolutionRecaller, termination, phaseList); - this.environmentMode = environmentMode; + public DefaultSolver(EnvironmentMode environmentMode, + DelegateScoreDirectorFactory delegateScoreDirectorFactory, + Supplier randomFactory, BestSolutionRecaller bestSolutionRecaller, + BasicPlumbingTermination basicPlumbingTermination, UniversalTermination termination, + List> phaseList, SolverScope solverScope, String moveThreadCountDescription) { + super(SolverContext.of(environmentMode, solverScope), delegateScoreDirectorFactory, bestSolutionRecaller, termination, + phaseList); this.randomFactory = randomFactory; this.basicPlumbingTermination = basicPlumbingTermination; this.solverScope = solverScope; @@ -63,16 +65,15 @@ public DefaultSolver(EnvironmentMode environmentMode, Supplier ran this.moveThreadCountDescription = moveThreadCountDescription; } - public EnvironmentMode getEnvironmentMode() { - return environmentMode; - } - public RandomSource getRandomSource() { return randomFactory.get(); } - public ScoreDirectorFactory getScoreDirectorFactory() { - return solverScope.getScoreDirector().getScoreDirectorFactory(); + @SuppressWarnings({ "unchecked", "resource" }) + public > ScoreDirectorFactory getScoreDirectorFactory() { + InnerScoreDirector scoreDirector = + (InnerScoreDirector) defaultSolverContext.scoreDirector(); + return scoreDirector.getScoreDirectorFactory(); } public SolverScope getSolverScope() { @@ -207,11 +208,11 @@ public void solvingStarted(SolverScope solverScope) { EventProducerId.solvingStarted()); LOGGER.info("Solving {}: time spent ({}), best score ({}), " - + "environment mode ({}), move thread count ({}), random ({}).", + + "default environment mode ({}), move thread count ({}), random ({}).", (startingSolverCount == 1 ? "started" : "restarted"), solverScope.calculateTimeMillisSpentUpToNow(), solverScope.getBestScore().raw(), - environmentMode.name(), + defaultSolverContext.environmentMode().name(), moveThreadCountDescription, randomFactory); if (LOGGER.isInfoEnabled()) { // Formatting is expensive here. @@ -319,7 +320,7 @@ public void outerSolvingEnded(SolverScope solverScope) { solverScope.getBestScore().raw(), solverScope.getMoveEvaluationSpeed(), phaseList.size(), - environmentMode.name(), + defaultSolverContext.environmentMode().name(), moveThreadCountDescription); // Must be kept open for doProblemFactChange solverScope.getScoreDirector().close(); 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 e3db6fba7a1..e6c5ba9f862 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 @@ -2,7 +2,6 @@ import java.time.Clock; import java.util.ArrayList; -import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -12,9 +11,11 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.SolverConfigOverride; import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig; import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; @@ -33,9 +34,8 @@ import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.phase.Phase; import ai.timefold.solver.core.impl.phase.PhaseFactory; -import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; +import ai.timefold.solver.core.impl.score.director.DelegateScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; -import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactoryFactory; import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; import ai.timefold.solver.core.impl.solver.random.DefaultRandomSource; import ai.timefold.solver.core.impl.solver.random.RandomSource; @@ -55,6 +55,25 @@ import io.micrometer.core.instrument.Tags; /** + * The default solver factory must maintain a default score director factory, + * as some solver components depend on this factory, + * including {@link SolverManager} and {@code TimefoldSolverBeanFactory}. + *

+ * The proposed approach establishes that the configuration defines a root environment mode, + * which is used to create the default score director factory. + * Since the phases can override the environment, + * the delegate factory will enable the creation of separate factories + * while maintaining a default one that is used for all other components. + *

+ * The necessity for a default environment mode can be illustrated by the following use case. + * Imagine a configuration that includes multiple phases, each with a different environment mode. + * If a Quarkus application needs to inject a {@link ConstraintMetaModel} + * instance, this instance depends on the score director factory, + * which in turn relies on the environment mode. + * If multiple phase environments exist, + * selecting one of these environments is not possible + * as this injection point is decoupled from the solving life cycle. + * * @param the solution type, the class with the {@link PlanningSolution} annotation * @see SolverFactory */ @@ -67,8 +86,11 @@ public final class DefaultSolverFactory implements SolverFactory solutionDescriptor; - private final ScoreDirectorFactory scoreDirectorFactory; + private final EnvironmentMode defaultEnvironmentMode; + private final DelegateScoreDirectorFactory delegateScoreDirectorFactory; + private final ScoreDirectorFactory defaultScoreDirectorFactory; private final DomainAccessType domainAccessType; + private final List metricsRequiringConstraintMatchList; public DefaultSolverFactory(SolverConfig solverConfig) { this(solverConfig, DomainAccessType.AUTO); @@ -77,10 +99,26 @@ public DefaultSolverFactory(SolverConfig solverConfig) { public DefaultSolverFactory(SolverConfig solverConfig, DomainAccessType domainAccessType) { this.domainAccessType = domainAccessType; this.clock = Objects.requireNonNullElse(solverConfig.getClock(), Clock.systemDefaultZone()); - this.solverConfig = Objects.requireNonNull(solverConfig, "The solverConfig (" + solverConfig + ") cannot be null."); + this.solverConfig = + Objects.requireNonNull(solverConfig, "The solverConfig (%s) cannot be null.".formatted(solverConfig)); + this.defaultEnvironmentMode = assertEnvironmentModeConfiguration(solverConfig); this.solutionDescriptor = buildSolutionDescriptor(); - // Caching score director factory as it potentially does expensive things. - this.scoreDirectorFactory = buildScoreDirectorFactory(); + var scoreDirectorFactoryConfig = + Objects.requireNonNullElseGet(solverConfig.getScoreDirectorFactoryConfig(), ScoreDirectorFactoryConfig::new); + this.metricsRequiringConstraintMatchList = determineMetricsRequiringConstraintMatch(solverConfig); + this.delegateScoreDirectorFactory = new DelegateScoreDirectorFactory<>( + Objects.requireNonNull(scoreDirectorFactoryConfig), !metricsRequiringConstraintMatchList.isEmpty()); + // Caching score director factory as it potentially does expensive things + this.defaultScoreDirectorFactory = + this.delegateScoreDirectorFactory.buildScoreDirectorFactory(defaultEnvironmentMode, solutionDescriptor); + } + + private static List determineMetricsRequiringConstraintMatch(SolverConfig solverConfig) { + var monitoringConfig = solverConfig.determineMetricConfig(); + var solverMetricList = Objects.requireNonNull(monitoringConfig.getSolverMetricList()); + return solverMetricList.stream() + .filter(SolverMetric::isMetricConstraintMatchBased) + .toList(); } public Clock getClock() { @@ -93,7 +131,7 @@ public SolutionDescriptor getSolutionDescriptor() { @SuppressWarnings("unchecked") public > ScoreDirectorFactory getScoreDirectorFactory() { - return (ScoreDirectorFactory) scoreDirectorFactory; + return (ScoreDirectorFactory) defaultScoreDirectorFactory; } @Override @@ -105,36 +143,25 @@ public Solver buildSolver(SolverConfigOverride configOverride) { var monitoringConfig = solverConfig.determineMetricConfig(); solverScope.setMonitoringTags(Tags.empty()); var solverMetricList = Objects.requireNonNull(monitoringConfig.getSolverMetricList()); - var metricsRequiringConstraintMatchSet = Collections. emptyList(); if (!solverMetricList.isEmpty()) { solverScope.setSolverMetricSet(EnumSet.copyOf(solverMetricList)); - metricsRequiringConstraintMatchSet = solverScope.getSolverMetricSet().stream() - .filter(SolverMetric::isMetricConstraintMatchBased) - .filter(solverScope::isMetricEnabled) - .toList(); } else { solverScope.setSolverMetricSet(EnumSet.noneOf(SolverMetric.class)); } - - var environmentMode = solverConfig.determineEnvironmentMode(); - var isStepAssertOrMore = environmentMode.isStepAssertOrMore(); - var constraintMatchEnabled = !metricsRequiringConstraintMatchSet.isEmpty() || isStepAssertOrMore; + var isStepAssertOrMore = defaultEnvironmentMode.isStepAssertOrMore(); + var constraintMatchEnabled = !metricsRequiringConstraintMatchList.isEmpty() || isStepAssertOrMore; if (constraintMatchEnabled && !isStepAssertOrMore) { LOGGER.info( "Enabling constraint matching as required by the enabled metrics ({}). This will impact solver performance.", - metricsRequiringConstraintMatchSet); + metricsRequiringConstraintMatchList); } - var castScoreDirector = scoreDirectorFactory.createScoreDirectorBuilder() - .withLookUpEnabled(true) // Custom phases and problem changes may rely on lookups. - .withConstraintMatchPolicy( - constraintMatchEnabled ? ConstraintMatchPolicy.ENABLED : ConstraintMatchPolicy.DISABLED) - .build(); - solverScope.setScoreDirector(castScoreDirector); - solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(castScoreDirector)); - + var scoreDirector = delegateScoreDirectorFactory.createScoreDirector(getScoreDirectorFactory()); + solverScope.setScoreDirector(scoreDirector); + solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(scoreDirector)); var moveThreadCount = resolveMoveThreadCount(true); - var bestSolutionRecaller = BestSolutionRecallerFactory.create(). buildBestSolutionRecaller(environmentMode); - var randomFactory = buildRandomSupplier(environmentMode); + var bestSolutionRecaller = + BestSolutionRecallerFactory.create(). buildBestSolutionRecaller(defaultEnvironmentMode); + var randomFactory = buildRandomSupplier(defaultEnvironmentMode); var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); var scoreDirectorFactoryConfig = solverConfig.getScoreDirectorFactoryConfig(); @@ -149,13 +176,13 @@ public Solver buildSolver(SolverConfigOverride configOverride) { var configPolicy = new HeuristicConfigPolicy.Builder() .withPreviewFeatureSet(previewFeaturesEnabled) - .withEnvironmentMode(environmentMode) + .withEnvironmentMode(defaultEnvironmentMode) .withMoveThreadCount(moveThreadCount) .withMoveThreadBufferSize(solverConfig.getMoveThreadBufferSize()) .withThreadFactoryClass(solverConfig.getThreadFactoryClass()) .withNearbyDistanceMeterClass(solverConfig.getNearbyDistanceMeterClass()) .withRandom(randomFactory.get()) - .withInitializingScoreTrend(scoreDirectorFactory.getInitializingScoreTrend()) + .withInitializingScoreTrend(defaultScoreDirectorFactory.getInitializingScoreTrend()) .withSolutionDescriptor(solutionDescriptor) .withClassInstanceCache(ClassInstanceCache.create()) .build(); @@ -163,8 +190,8 @@ public Solver buildSolver(SolverConfigOverride configOverride) { var termination = buildTermination(basicPlumbingTermination, configPolicy, configOverride); var phaseList = buildPhaseList(configPolicy, bestSolutionRecaller, termination); - return new DefaultSolver<>(environmentMode, randomFactory, bestSolutionRecaller, basicPlumbingTermination, - (UniversalTermination) termination, phaseList, solverScope, + return new DefaultSolver<>(defaultEnvironmentMode, delegateScoreDirectorFactory, randomFactory, bestSolutionRecaller, + basicPlumbingTermination, (UniversalTermination) termination, phaseList, solverScope, moveThreadCount == null ? SolverConfig.MOVE_THREAD_COUNT_NONE : Integer.toString(moveThreadCount)); } @@ -182,7 +209,7 @@ private SolverTermination buildTermination(BasicPlumbingTermination configPolicy, SolverConfigOverride solverConfigOverride) { var terminationConfig = Objects.requireNonNullElseGet(solverConfigOverride.getTerminationConfig(), () -> Objects.requireNonNullElseGet(solverConfig.getTerminationConfig(), TerminationConfig::new)); - return TerminationFactory. create(terminationConfig) + return TerminationFactory. create(Objects.requireNonNull(terminationConfig)) .buildTermination(configPolicy, basicPlumbingTermination); } @@ -205,22 +232,14 @@ private SolutionDescriptor buildSolutionDescriptor() { solverConfig.getEntityClassList()); } - private > ScoreDirectorFactory buildScoreDirectorFactory() { - var environmentMode = solverConfig.determineEnvironmentMode(); - var scoreDirectorFactoryConfig_ = - Objects.requireNonNullElseGet(solverConfig.getScoreDirectorFactoryConfig(), ScoreDirectorFactoryConfig::new); - var scoreDirectorFactoryFactory = new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig_); - return scoreDirectorFactoryFactory.buildScoreDirectorFactory(environmentMode, solutionDescriptor); - } - - public Supplier buildRandomSupplier(EnvironmentMode environmentMode_) { - var randomSeed_ = solverConfig.getRandomSeed(); - if (randomSeed_ == null && environmentMode_ != EnvironmentMode.NON_REPRODUCIBLE) { - randomSeed_ = DEFAULT_RANDOM_SEED; - } else if (randomSeed_ == null) { - randomSeed_ = RandomGenerator.getDefault().nextLong(); + Supplier buildRandomSupplier(EnvironmentMode environmentMode) { + var randomSeed = solverConfig.getRandomSeed(); + if (randomSeed == null && environmentMode != EnvironmentMode.NON_REPRODUCIBLE) { + randomSeed = DEFAULT_RANDOM_SEED; + } else if (randomSeed == null) { + randomSeed = RandomGenerator.getDefault().nextLong(); } - return DefaultRandomSource.seededSupplier(randomSeed_); + return DefaultRandomSource.seededSupplier(randomSeed); } public List> buildPhaseList(HeuristicConfigPolicy configPolicy, @@ -272,6 +291,59 @@ public void ensurePreviewFeature(PreviewFeature previewFeature) { HeuristicConfigPolicy.ensurePreviewFeature(previewFeature, solverConfig.getEnablePreviewFeatureSet()); } + private static EnvironmentMode assertEnvironmentModeConfiguration(SolverConfig solverConfig) { + var defaultEnvironmentMode = solverConfig.determineEnvironmentMode(); + var phaseConfigList = solverConfig.getPhaseConfigList(); + if (ConfigUtils.isEmptyCollection(phaseConfigList)) { + return defaultEnvironmentMode; + } + var phaseEnvironmentList = + phaseConfigList.stream() + .map(phaseConfig -> Objects.requireNonNullElse(phaseConfig.getEnvironmentMode(), + defaultEnvironmentMode)) + .toList(); + if (defaultEnvironmentMode == EnvironmentMode.NON_REPRODUCIBLE + && phaseEnvironmentList.stream().anyMatch(environmentMode -> environmentMode != defaultEnvironmentMode)) { + // If the default environment is non-reproducible, + // then all phase environment modes must also be non-reproducible + throw new IllegalStateException( + "The default environment mode is (%s), and all phase environments [%s] must also be non-reproducible." + .formatted(defaultEnvironmentMode.name(), + String.join(", ", phaseEnvironmentList.stream().map(EnvironmentMode::name).toList()))); + } + // If none of the phase environments use the default environment, we fail fast. + var checkDefaultEnvironment = phaseEnvironmentList.isEmpty(); + for (var phaseEnvironment : phaseEnvironmentList) { + if (phaseEnvironment == defaultEnvironmentMode) { + checkDefaultEnvironment = true; + break; + } + } + if (!checkDefaultEnvironment) { + throw new IllegalStateException(""" + The default environment mode (%s) is not used in any of the defined phases environment modes [%s]. + Maybe adjust the solver config's default environment mode. + Maybe adjust at least one of the phase environment modes to match the default environment mode (%s)""" + .formatted( + defaultEnvironmentMode.name(), + String.join(", ", phaseEnvironmentList.stream().map(EnvironmentMode::name).toList()), + defaultEnvironmentMode.name())); + } + var invalidPhaseEnvironmentList = new ArrayList(phaseConfigList.size()); + for (var phaseEnvironment : phaseEnvironmentList) { + if (phaseEnvironment.ordinal() > defaultEnvironmentMode.ordinal()) { + invalidPhaseEnvironmentList.add(phaseEnvironment.name()); + } + } + if (!invalidPhaseEnvironmentList.isEmpty()) { + // The phase environments must have an assertion level greater than or equal to the default environment level + throw new IllegalStateException( + "The phase environments must have an assertion level higher than or equal to the default environment level (%s). The following phase environment modes are not valid: [%s]." + .formatted(defaultEnvironmentMode.name(), String.join(", ", invalidPhaseEnvironmentList))); + } + return defaultEnvironmentMode; + } + // Required for testability as final classes cannot be mocked. static class MoveThreadCountResolver { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java index 91f645962b5..b04651d32b8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.event.EventProducerId; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -24,22 +25,16 @@ public class BestSolutionRecaller extends PhaseLifecycleListenerAdapt protected SolverEventSupport solverEventSupport; - public void setAssertInitialScoreFromScratch(boolean assertInitialScoreFromScratch) { - this.assertInitialScoreFromScratch = assertInitialScoreFromScratch; - } - - public void setAssertShadowVariablesAreNotStale(boolean assertShadowVariablesAreNotStale) { - this.assertShadowVariablesAreNotStale = assertShadowVariablesAreNotStale; - } - - public void setAssertBestScoreIsUnmodified(boolean assertBestScoreIsUnmodified) { - this.assertBestScoreIsUnmodified = assertBestScoreIsUnmodified; - } - public void setSolverEventSupport(SolverEventSupport solverEventSupport) { this.solverEventSupport = solverEventSupport; } + public void enableAssertions(EnvironmentMode environmentMode) { + assertInitialScoreFromScratch = environmentMode.isFullyAsserted(); + assertShadowVariablesAreNotStale = environmentMode.isFullyAsserted(); + assertBestScoreIsUnmodified = environmentMode.isFullyAsserted(); + } + // ************************************************************************ // Worker methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecallerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecallerFactory.java index c56f244290b..ce037d34254 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecallerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecallerFactory.java @@ -9,12 +9,8 @@ public static BestSolutionRecallerFactory create() { } public BestSolutionRecaller buildBestSolutionRecaller(EnvironmentMode environmentMode) { - BestSolutionRecaller bestSolutionRecaller = new BestSolutionRecaller<>(); - if (environmentMode.isFullyAsserted()) { - bestSolutionRecaller.setAssertInitialScoreFromScratch(true); - bestSolutionRecaller.setAssertShadowVariablesAreNotStale(true); - bestSolutionRecaller.setAssertBestScoreIsUnmodified(true); - } + var bestSolutionRecaller = new BestSolutionRecaller(); + bestSolutionRecaller.enableAssertions(environmentMode); return bestSolutionRecaller; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index f041e445c0b..b70210dbc6c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -193,7 +193,7 @@ public > InnerScore calculateScore() { } public void assertScoreFromScratch(Solution_ solution) { - scoreDirector.getScoreDirectorFactory().assertScoreFromScratch(solution); + scoreDirector.assertScoreFromScratch(solution); } @SuppressWarnings("unchecked") diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index a5d10b20c96..6848a910978 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -291,6 +291,8 @@ + + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolderTest.java new file mode 100644 index 00000000000..ddb4f643d01 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolderTest.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.testdomain.list.TestdataListSolution; + +import org.junit.jupiter.api.Test; + +class ListVariableStateSupplyHolderTest { + + @SuppressWarnings("unchecked") + @Test + void demandsOnPhaseStartedAndCancelsOnPhaseEnded() { + ListVariableDescriptor listVariableDescriptor = mock(ListVariableDescriptor.class); + var stateDemand = new ListVariableStateDemand<>(listVariableDescriptor); + doReturn(stateDemand).when(listVariableDescriptor).getStateDemand(); + + ListVariableStateSupply listVariableStateSupply = + mock(ListVariableStateSupply.class); + SupplyManager supplyManager = mock(SupplyManager.class); + doReturn(listVariableStateSupply).when(supplyManager).demand(stateDemand); + + InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); + doReturn(supplyManager).when(scoreDirector).getSupplyManager(); + + AbstractPhaseScope phaseScope = mock(AbstractPhaseScope.class); + doReturn(scoreDirector).when(phaseScope).getScoreDirector(); + + var holder = new ListVariableStateSupplyHolder<>(listVariableDescriptor); + + // Not yet demanded: get() must fail fast rather than silently return null. + assertThatNullPointerException().isThrownBy(holder::get) + .withMessageContaining("not initialized yet"); + + holder.phaseStarted(phaseScope); + assertThat(holder.get()).isSameAs(listVariableStateSupply); + verify(supplyManager).demand(stateDemand); + + holder.phaseEnded(phaseScope); + verify(supplyManager).cancel(stateDemand); + assertThatNullPointerException().isThrownBy(holder::get) + .withMessageContaining("not initialized yet"); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java index 6e323af90f5..835c18d067b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java @@ -96,7 +96,8 @@ void original() { var selector = new ElementDestinationSelector<>(entitySelector, valueSelector, false); - solvingStarted(selector, scoreDirector); + var solverScope = solvingStarted(selector, scoreDirector); + phaseStarted(selector, solverScope); // Entity order: [A, B, C] // Value order: [3, 1, 2] @@ -147,7 +148,8 @@ void random() { 2, // => C[0] -1); // (not tested) - solvingStarted(selector, scoreDirector, random); + var solverScope = solvingStarted(selector, scoreDirector, random); + phaseStarted(selector, solverScope); // Initial state: // - A [1, 2] @@ -560,7 +562,9 @@ void emptyIfThereAreNoEntities() { mockIterableValueSelector(TestdataListEntity.buildVariableDescriptorForValueList(), v1, v2, v3); var randomSelector = new ElementDestinationSelector<>(entitySelector, valueSelector, true); - solvingStarted(randomSelector, scoreDirector); + var solverScope = solvingStarted(randomSelector, scoreDirector); + phaseStarted(randomSelector, solverScope); + assertEmptyNeverEndingIterableSelector(randomSelector, 0); var originalSelector = new ElementDestinationSelector<>(entitySelector, valueSelector, false); @@ -589,7 +593,9 @@ void notEmptyIfThereAreEntities() { var randomSelector = new ElementDestinationSelector<>(entitySelector, valueSelector, true); var random = new TestRandom(0, 1); - solvingStarted(randomSelector, scoreDirector, random); + var solverScope = solvingStarted(randomSelector, scoreDirector, random); + phaseStarted(randomSelector, solverScope); + // Do not assert all codes to prevent exhausting the iterator. assertCodesOfNeverEndingIterableSelector(randomSelector, 2, "A[0]"); } @@ -617,7 +623,8 @@ void notEmptyIfThereAreEntitiesWithPinning() { var randomSelector = new ElementDestinationSelector<>(entitySelector, valueSelector, true); var random = new TestRandom(0, 1); - solvingStarted(randomSelector, scoreDirector, random); + var solverScope = solvingStarted(randomSelector, scoreDirector, random); + phaseStarted(randomSelector, solverScope); // Do not assert all codes to prevent exhausting the iterator. assertCodesOfNeverEndingIterableSelector(randomSelector, 2, "A[0]"); } @@ -678,7 +685,9 @@ void discardOldValues() { // Picks value selector twice var random = new TestRandom(5, 5, 5, 5); - solvingStarted(selector, scoreDirector, random); + var solverScope = solvingStarted(selector, scoreDirector, random); + phaseStarted(selector, solverScope); + assertAllCodesOfIterator(selector.iterator(), "B[1]", "B[1]"); // Even using only the value selector, @@ -707,7 +716,8 @@ void discardOldValuesAndResetState() { var selector = new ElementDestinationSelector<>(entitySelector, replayingValueSelector, valueSelector, true, false); // Value 0 makes the iterator to always request an entity from the related iterator var random = new TestRandom(0, 0); - solvingStarted(selector, scoreDirector, random); + var solverScope = solvingStarted(selector, scoreDirector, random); + phaseStarted(selector, solverScope); var iterator = selector.iterator(); // entityIterator returns a assertThat(iterator.hasNext()).isTrue(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java index 5fb7b0c035b..982e377b647 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java @@ -67,7 +67,8 @@ void randomUnrestricted() { var random = new TestRandom(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0); - solvingStarted(selector, scoreDirector, random); + var solverScope = solvingStarted(selector, scoreDirector, random); + phaseStarted(selector, solverScope); // Every possible subList is selected. assertCodesOfNeverEndingIterableSelector(selector, subListCount, @@ -153,7 +154,8 @@ void randomAllowsUnassignedValues() { var random = new TestRandom(0, 1, 2, 3, 4, 5, 0); - solvingStarted(selector, scoreDirector, random); + var solverScope = solvingStarted(selector, scoreDirector, random); + phaseStarted(selector, solverScope); // Every possible subList is selected. assertCodesOfNeverEndingIterableSelector(selector, subListCount, @@ -192,7 +194,8 @@ void randomWithSubListSizeBounds() { var random = new TestRandom(0, 1, 2, 3, 4, 5, 6, 0); - solvingStarted(selector, scoreDirector, random); + var solverScope = solvingStarted(selector, scoreDirector, random); + phaseStarted(selector, solverScope); // Every possible subList is selected. assertCodesOfNeverEndingIterableSelector(selector, subListCount, diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java index 01615518f44..c6620af0932 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java @@ -79,7 +79,8 @@ void original() { ElementPosition.of(a, 1)), false); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(moveSelector, solverScope); // Value order: [3, 1, 2] // Entity order: [A, B, C] @@ -134,7 +135,7 @@ void originalWithEntityValueRange() { var moveSelector = new ListChangeMoveSelector<>(mimicRecordingValueSelector, destinationSelector, false); var solverScope = solvingStarted(moveSelector, scoreDirector, mimicRecordingValueSelector, destinationSelector); - phaseStarted(solverScope, mimicRecordingValueSelector, destinationSelector); + phaseStarted(solverScope, moveSelector, mimicRecordingValueSelector, destinationSelector); // Not testing size; filtering selector doesn't and can't report correct size unless iterating over all values. assertAllCodesOfMoveSelectorWithoutSize(moveSelector, @@ -269,7 +270,8 @@ void originalAllowsUnassignedValues() { ElementPosition.unassigned()), false); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(solverScope, moveSelector); // First try all destinations for v3 (which is originally at C[0]), // then v1 (originally at A[1]), @@ -330,7 +332,7 @@ void originalAllowsUnassignedValuesWithEntityValueRange() { var moveSelector = new ListChangeMoveSelector<>(mimicRecordingValueSelector, destinationSelector, false); var solverScope = solvingStarted(moveSelector, scoreDirector, mimicRecordingValueSelector, destinationSelector); - phaseStarted(solverScope, mimicRecordingValueSelector, destinationSelector); + phaseStarted(solverScope, moveSelector, mimicRecordingValueSelector, destinationSelector); // Not testing size; filtering selector doesn't and can't report correct size unless iterating over all values. assertAllCodesOfMoveSelectorWithoutSize(moveSelector, "1 {A[1]->A[0]}", @@ -372,7 +374,8 @@ void random() { ElementPosition.of(a, 2)), true); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(moveSelector, solverScope); // Initial state: // - A [1, 2] @@ -575,7 +578,8 @@ void randomAllowsUnassignedValues() { ElementPosition.unassigned()), true); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, "2 {A[1]->B[0]}", diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java index 6516a77331b..55d62cdac28 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java @@ -66,7 +66,8 @@ void original() { mockIterableValueSelector(listVariableDescriptor, v3, v1, v2), false); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(moveSelector, solverScope); // Value order: [3, 1, 2] // Entity order: [A, B, C] @@ -220,7 +221,8 @@ void originalAllowsUnassignedValues() { mockIterableValueSelector(listVariableDescriptor, v4, v3, v2, v1), false); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(moveSelector, solverScope); // Tests each move from the product of the two value selectors. assertAllCodesOfMoveSelectorWithoutSize(moveSelector, @@ -297,7 +299,8 @@ void random() { mockIterableValueSelector(listVariableDescriptor, v1, v2, v3, v1, v2, v3, v1, v2, v3, v1), true); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, "2 {A[1]} <-> 1 {A[0]}", @@ -502,7 +505,8 @@ void randomAllowsUnassignedValues() { mockIterableValueSelector(listVariableDescriptor, v1, v2, v3, v4, v1, v2, v3, v4, v1, v2, v3, v1, v4), true); - solvingStarted(moveSelector, scoreDirector); + var solverScope = solvingStarted(moveSelector, scoreDirector); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, "2 {A[0]} <-> 1 {A[1]}", diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIteratorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIteratorTest.java index ef4750255b2..ec5d7c66580 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIteratorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIteratorTest.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list; +import static ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils.phaseStarted; import static ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils.solvingStarted; import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.getListVariableDescriptor; import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.mockEntitySelector; @@ -46,7 +47,9 @@ void iterator() { var destinationSelector = new ElementDestinationSelector<>(entitySelector, destinationValueSelector, true); var random = new TestRandom(3, 0, 1); - solvingStarted(destinationSelector, scoreDirector, random); + var solverScope = solvingStarted(destinationSelector, scoreDirector, random); + phaseStarted(destinationSelector, solverScope); + var randomListChangeIterator = new RandomListChangeIterator<>( scoreDirector.getSupplyManager().demand(listVariableDescriptor.getStateDemand()), sourceValueSelector, diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java index 4d106a2ad16..81981990653 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java @@ -68,7 +68,8 @@ void randomUnrestricted() { var random = new TestRandom(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); // Every possible subList is selected. assertCodesOfNeverEndingMoveSelector(moveSelector, subListCount * destinationSize, @@ -139,7 +140,8 @@ void randomAllowsUnassignedValues() { 2, 2, 2, 2, 2, 2, 0); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); // Every possible subList is selected. assertCodesOfNeverEndingMoveSelector(moveSelector, subListCount * destinationSize, @@ -218,7 +220,8 @@ void randomReversing() { 9, 0, -1, -1); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); // Every possible subList is selected; some moves are reversing. assertCodesOfNeverEndingMoveSelector(moveSelector, moveSelectorSize, @@ -266,7 +269,8 @@ void randomWithSubListSizeBounds() { var random = new TestRandom(0, 1, 2, 3, 4, -1); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); // Only subLists bigger than 1 and smaller than 4 are selected. assertCodesOfNeverEndingMoveSelector(moveSelector, subListCount * destinationSize, @@ -339,7 +343,8 @@ void skipSubListsSmallerThanMinimumSize() { var random = new TestRandom(0, 1, -1); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); // Only subLists of size 2 are selected. assertCodesOfNeverEndingMoveSelector(moveSelector, diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java index b6f84690b2f..64942d3643b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java @@ -89,7 +89,8 @@ void sameEntityUnrestricted() { 9, 0, 0, 0); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, subListCount * subListCount, "{A[0+4]} <-> {A[0+4]}", @@ -162,7 +163,8 @@ void reversing() { 3, 0, 1, 0, 0, 0); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, subListCount * subListCount * 2, "{A[0+3]} <-reversing-> {B[1+1]}", @@ -222,7 +224,8 @@ void sameEntityWithSubListSizeBounds() { 4, 0, 0, 0); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, subListCount * subListCount, "{A[0+3]} <-> {A[0+3]}", @@ -317,7 +320,8 @@ void skipSubListsSmallerThanMinimumSize() { 1, 1, 0, 0); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, subListCount * subListCount, "{A[0+2]} <-> {A[0+2]}", @@ -375,7 +379,8 @@ void allowsUnassignedValues() { 0, 0, 0, 1, 0, 2, 0, 0, 0, 0); - solvingStarted(moveSelector, scoreDirector, random); + var solverScope = solvingStarted(moveSelector, scoreDirector, random); + phaseStarted(moveSelector, solverScope); assertCodesOfNeverEndingMoveSelector(moveSelector, (long) subListCount * subListCount, "{A[0+2]} <-> {A[0+2]}", 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 c00a3978f73..2625aeb1902 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 @@ -54,7 +54,8 @@ void buildCompositeAcceptor() { when(heuristicConfigPolicy.getScoreDefinition()).thenReturn(scoreDefinition); AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); - Acceptor acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + Acceptor acceptor = + acceptorFactory.buildAcceptor(heuristicConfigPolicy, heuristicConfigPolicy.getEnvironmentMode()); assertThat(acceptor).isExactlyInstanceOf(CompositeAcceptor.class); CompositeAcceptor compositeAcceptor = (CompositeAcceptor) acceptor; assertThat(compositeAcceptor.acceptorList) @@ -67,7 +68,9 @@ void buildCompositeAcceptor() { @Test void noAcceptorConfigured_throwsException() { AcceptorFactory acceptorFactory = AcceptorFactory.create(new LocalSearchAcceptorConfig()); - assertThatIllegalArgumentException().isThrownBy(() -> acceptorFactory.buildAcceptor(mock(HeuristicConfigPolicy.class))) + assertThatIllegalArgumentException() + .isThrownBy( + () -> acceptorFactory.buildAcceptor(mock(HeuristicConfigPolicy.class), EnvironmentMode.PHASE_ASSERT)) .withMessageContaining("The acceptor does not specify any acceptorType"); } @@ -77,13 +80,13 @@ void lateAcceptanceAcceptor() { .withAcceptorTypeList(List.of(AcceptorType.LATE_ACCEPTANCE)); HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); - var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy, EnvironmentMode.PHASE_ASSERT); assertThat(acceptor).isExactlyInstanceOf(LateAcceptanceAcceptor.class); localSearchAcceptorConfig = new LocalSearchAcceptorConfig() .withLateAcceptanceSize(10); acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); - acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy, EnvironmentMode.PHASE_ASSERT); assertThat(acceptor).isExactlyInstanceOf(LateAcceptanceAcceptor.class); } @@ -93,14 +96,14 @@ void diversifiedLateAcceptanceAcceptor() { .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)); HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); - var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy, EnvironmentMode.PHASE_ASSERT); 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); + acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy, EnvironmentMode.PHASE_ASSERT); assertThat(acceptor).isExactlyInstanceOf(DiversifiedLateAcceptanceAcceptor.class); doThrow(new IllegalStateException()).when(heuristicConfigPolicy).ensurePreviewFeature(any()); @@ -108,24 +111,25 @@ void diversifiedLateAcceptanceAcceptor() { .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) .withLateAcceptanceSize(10); AcceptorFactory badAcceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); - assertThatIllegalStateException().isThrownBy(() -> badAcceptorFactory.buildAcceptor(heuristicConfigPolicy)); + assertThatIllegalStateException() + .isThrownBy(() -> badAcceptorFactory.buildAcceptor(heuristicConfigPolicy, EnvironmentMode.PHASE_ASSERT)); } @Test - void valueTabuWithoutSizes_throwsException() { + 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))); + .isThrownBy(() -> factory.buildAcceptor(mock(HeuristicConfigPolicy.class), EnvironmentMode.PHASE_ASSERT)); } @Test - void moveTabuWithoutSizes_throwsException() { + 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))); + .isThrownBy(() -> factory.buildAcceptor(mock(HeuristicConfigPolicy.class), EnvironmentMode.PHASE_ASSERT)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java index 5fcfec9505a..97c829327a0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java @@ -69,11 +69,13 @@ void changeMoveBasedLocalSearch() { List.of(new ChangeMoveProvider<>(variableMetaModel))); var acceptor = AcceptorFactory. create(new LocalSearchAcceptorConfig().withLateAcceptanceSize(400)) - .buildAcceptor(heuristicConfigPolicy); + .buildAcceptor(heuristicConfigPolicy, heuristicConfigPolicy.getEnvironmentMode()); var forager = LocalSearchForagerFactory . create(new LocalSearchForagerConfig().withAcceptedCountLimit(1)).buildForager(); var localSearchDecider = new LocalSearchDecider<>("", termination, moveRepository, acceptor, forager); - var localSearchPhase = new DefaultLocalSearchPhase.Builder<>(0, "", termination, localSearchDecider).build(); + var localSearchPhase = + new DefaultLocalSearchPhase.Builder<>(0, EnvironmentMode.PHASE_ASSERT, "", termination, localSearchDecider) + .build(); // Generates a solution whose entities' values are all set to the second value. // The easy calculator penalizes this. diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactoryTest.java similarity index 87% rename from core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactoryTest.java index 1f2076cfe1a..ce72a964391 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactoryTest.java @@ -13,15 +13,17 @@ import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.incremental.IncrementalScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; +import ai.timefold.solver.core.testconstraint.DummyConstraintProvider; import ai.timefold.solver.core.testdomain.TestdataSolution; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; import org.junit.jupiter.api.Test; -class ScoreDirectorFactoryFactoryTest { +class DelegateScoreDirectorFactoryTest { @Test void multipleScoreCalculations_throwsException() { @@ -35,7 +37,7 @@ void multipleScoreCalculations_throwsException() { private ScoreDirectorFactory buildTestdataScoreDirectoryFactory(ScoreDirectorFactoryConfig config, EnvironmentMode environmentMode) { - return new ScoreDirectorFactoryFactory(config) + return new DelegateScoreDirectorFactory(config) .buildScoreDirectorFactory(environmentMode, TestdataSolution.buildSolutionDescriptor()); } @@ -44,6 +46,17 @@ void multipleScoreCalculations_throwsException() { return buildTestdataScoreDirectoryFactory(config, EnvironmentMode.PHASE_ASSERT); } + @Test + void constraintMatchEnabledPerPhaseEnvironmentMode() { + var config = new ScoreDirectorFactoryConfig().withConstraintProviderClass(DummyConstraintProvider.class); + var delegateScoreDirectorFactory = new DelegateScoreDirectorFactory(config, false); + var phaseScoreDirectorFactory = delegateScoreDirectorFactory.buildScoreDirectorFactory(EnvironmentMode.FULL_ASSERT, + TestdataSolution.buildSolutionDescriptor()); + try (var scoreDirector = delegateScoreDirectorFactory.createScoreDirector(phaseScoreDirectorFactory)) { + assertThat(scoreDirector.getConstraintMatchPolicy()).isEqualTo(ConstraintMatchPolicy.ENABLED); + } + } + @Test void constraintStreamsBavet() { var config = new ScoreDirectorFactoryConfig() @@ -167,15 +180,17 @@ public void setIntProperty(int intProperty) { @Override public void resetWorkingSolution(TestdataSolution workingSolution) { - + // No actions } @Override public void beforeVariableChanged(Object entity, String variableName) { + // No actions } @Override public void afterVariableChanged(Object entity, String variableName) { + // No actions } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java index 619f9507f57..eb6399abf6f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java @@ -10,8 +10,8 @@ import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorSemanticsTest; +import ai.timefold.solver.core.impl.score.director.DelegateScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; -import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactoryFactory; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesEasyScoreCalculator; import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesSolution; @@ -32,7 +32,7 @@ final class EasyScoreDirectorSemanticsTest extends AbstractScoreDirectorSemantic var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withEasyScoreCalculatorClass(TestdataConstraintWeightOverridesEasyScoreCalculator.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory( + new DelegateScoreDirectorFactory( scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } @@ -44,7 +44,7 @@ final class EasyScoreDirectorSemanticsTest extends AbstractScoreDirectorSemantic var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withEasyScoreCalculatorClass(TestdataPinnedListEasyScoreCalculator.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); + new DelegateScoreDirectorFactory(scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } @@ -55,7 +55,7 @@ final class EasyScoreDirectorSemanticsTest extends AbstractScoreDirectorSemantic var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withEasyScoreCalculatorClass(TestdataPinnedWithIndexListEasyScoreCalculator.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); + new DelegateScoreDirectorFactory(scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } @@ -79,7 +79,7 @@ void easyScoreCalculatorWithCustomProperties() { private ScoreDirectorFactory buildTestdataScoreDirectoryFactory( ScoreDirectorFactoryConfig config, EnvironmentMode environmentMode) { - return new ScoreDirectorFactoryFactory(config) + return new DelegateScoreDirectorFactory(config) .buildScoreDirectorFactory(environmentMode, TestdataSolution.buildSolutionDescriptor()); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java index 3aa3c4b50f8..b773d5c899f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java @@ -13,8 +13,8 @@ import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorSemanticsTest; +import ai.timefold.solver.core.impl.score.director.DelegateScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; -import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactoryFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesSolution; import ai.timefold.solver.core.testdomain.list.pinned.TestdataPinnedListEntity; @@ -34,7 +34,7 @@ final class IncrementalScoreDirectorSemanticsTest extends AbstractScoreDirectorS var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withIncrementalScoreCalculatorClass(TestdataConstraintWeightOverridesIncrementalScoreCalculator.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory( + new DelegateScoreDirectorFactory( scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } @@ -45,7 +45,7 @@ protected ScoreDirectorFactory buildSco var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withIncrementalScoreCalculatorClass(TestdataPinnedListIncrementalScoreCalculator.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); + new DelegateScoreDirectorFactory(scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } @@ -56,7 +56,7 @@ protected ScoreDirectorFactory buildSco var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withIncrementalScoreCalculatorClass(TestdataPinnedWithIndexListIncrementalScoreCalculator.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); + new DelegateScoreDirectorFactory(scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java index e5f0d0e1ab8..cd195be2f63 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.definition.SimpleScoreDefinition; @@ -61,6 +62,7 @@ private IncrementalScoreDirectorFactory mockIncrementalScor when(factory.getScoreDefinition()).thenReturn(new SimpleScoreDefinition()); SolutionDescriptor solutionDescriptor = mock(SolutionDescriptor.class); when(factory.getSolutionDescriptor()).thenReturn(solutionDescriptor); + when(factory.getEnvironmentMode()).thenReturn(EnvironmentMode.PHASE_ASSERT); return factory; } @@ -124,10 +126,12 @@ public void resetWorkingSolution(Object workingSolution) { @Override public void beforeVariableChanged(Object entity, String variableName) { + // No action needed } @Override public void afterVariableChanged(Object entity, String variableName) { + // No action needed } @Override @@ -168,10 +172,12 @@ public void resetWorkingSolution(Object workingSolution) { @Override public void beforeVariableChanged(Object entity, String variableName) { + // No action needed } @Override public void afterVariableChanged(Object entity, String variableName) { + // No action needed } @Override @@ -213,10 +219,12 @@ public void resetWorkingSolution(Object workingSolution) { @Override public void beforeVariableChanged(Object entity, String variableName) { + // No action needed } @Override public void afterVariableChanged(Object entity, String variableName) { + // No action needed } @Override @@ -264,10 +272,12 @@ public void resetWorkingSolution(Object workingSolution) { @Override public void beforeVariableChanged(Object entity, String variableName) { + // No action needed } @Override public void afterVariableChanged(Object entity, String variableName) { + // No action needed } @Override @@ -311,10 +321,12 @@ public void resetWorkingSolution(Object workingSolution) { @Override public void beforeVariableChanged(Object entity, String variableName) { + // No action needed } @Override public void afterVariableChanged(Object entity, String variableName) { + // No action needed } @Override @@ -358,10 +370,12 @@ public void resetWorkingSolution(Object workingSolution) { @Override public void beforeVariableChanged(Object entity, String variableName) { + // No action needed } @Override public void afterVariableChanged(Object entity, String variableName) { + // No action needed } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/stream/ConstraintStreamsBavetScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/stream/ConstraintStreamsBavetScoreDirectorSemanticsTest.java index 08bacb8906f..d577c4682d5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/stream/ConstraintStreamsBavetScoreDirectorSemanticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/stream/ConstraintStreamsBavetScoreDirectorSemanticsTest.java @@ -5,8 +5,8 @@ import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorSemanticsTest; +import ai.timefold.solver.core.impl.score.director.DelegateScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; -import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactoryFactory; import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesConstraintProvider; import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesSolution; import ai.timefold.solver.core.testdomain.list.pinned.TestdataPinnedListConstraintProvider; @@ -23,7 +23,7 @@ final class ConstraintStreamsBavetScoreDirectorSemanticsTest extends AbstractSco var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withConstraintProviderClass(TestdataConstraintWeightOverridesConstraintProvider.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory( + new DelegateScoreDirectorFactory( scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } @@ -35,7 +35,7 @@ final class ConstraintStreamsBavetScoreDirectorSemanticsTest extends AbstractSco var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withConstraintProviderClass(TestdataPinnedListConstraintProvider.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); + new DelegateScoreDirectorFactory(scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } @@ -46,7 +46,7 @@ final class ConstraintStreamsBavetScoreDirectorSemanticsTest extends AbstractSco var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() .withConstraintProviderClass(TestdataPinnedWithIndexListConstraintProvider.class); var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); + new DelegateScoreDirectorFactory(scoreDirectorFactoryConfig); return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java index e1b5108e66c..c8afc141ea2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java @@ -18,6 +18,7 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.invalid.noentity.TestdataNoEntitySolution; +import ai.timefold.solver.core.testutil.PlannerTestUtils; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; @@ -165,4 +166,56 @@ void testInvalidConstraintProfilingWithoutEnterprise() { "remove constraintStreamProfilingEnabled from the solver configuration"); } + @Test + void assertEnvironmentModeWithoutPhases() { + var solverConfig = new SolverConfig() + .withSolutionClass(TestdataSolution.class) + .withEntityClasses(TestdataEntity.class) + .withEasyScoreCalculatorClass(DummyEasyScoreCalculator.class) + .withEnvironmentMode(EnvironmentMode.FULL_ASSERT); + assertThatCode(() -> new DefaultSolverFactory<>(solverConfig)).doesNotThrowAnyException(); + } + + @Test + void assertEnvironmentModeWithValidPhases() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withEnvironmentMode(EnvironmentMode.FULL_ASSERT); + // The default environment mode must be used by at least one phase, + // and every phase must be at least as strict as the default. + solverConfig.getPhaseConfigList().getFirst().setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.TRACKED_FULL_ASSERT); + assertThatCode(() -> new DefaultSolverFactory<>(solverConfig)).doesNotThrowAnyException(); + } + + @Test + void assertEnvironmentWithNonReproducibleAndMismatchingPhase() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withEnvironmentMode(EnvironmentMode.NON_REPRODUCIBLE); + solverConfig.getPhaseConfigList().get(0).setEnvironmentMode(EnvironmentMode.NON_REPRODUCIBLE); + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.NO_ASSERT); + assertThatCode(() -> new DefaultSolverFactory<>(solverConfig)) + .hasMessageContaining("must also be non-reproducible"); + } + + @Test + void assertEnvironmentModeWithDefaultNotUsedByAnyPhase() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withEnvironmentMode(EnvironmentMode.STEP_ASSERT); + solverConfig.getPhaseConfigList().get(0).setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.NON_INTRUSIVE_FULL_ASSERT); + assertThatCode(() -> new DefaultSolverFactory<>(solverConfig)) + .hasMessageContaining("is not used in any of the defined phases environment modes"); + } + + @Test + void assertEnvironmentModeWithPhaseLessStrictThanDefault() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withEnvironmentMode(EnvironmentMode.STEP_ASSERT); + solverConfig.getPhaseConfigList().get(0).setEnvironmentMode(EnvironmentMode.STEP_ASSERT); + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.NO_ASSERT); + assertThatCode(() -> new DefaultSolverFactory<>(solverConfig)) + .hasMessageContaining( + "must have an assertion level higher than or equal to the default environment level"); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index af7e60b60da..10eb745cf89 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -16,6 +16,7 @@ import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.LongAdder; import java.util.random.RandomGenerator; @@ -68,9 +69,14 @@ import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.heuristic.move.AbstractSelectorBasedMove; import ai.timefold.solver.core.impl.heuristic.selector.move.factory.MoveIteratorFactory; +import ai.timefold.solver.core.impl.phase.Phase; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.score.DummySimpleScoreEasyScoreCalculator; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.ScoreDirector; import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.preview.api.move.builtin.Moves; import ai.timefold.solver.core.preview.api.neighborhood.Neighborhood; @@ -570,6 +576,46 @@ void solveWithProblemChange() throws InterruptedException { } } + @Test + void solvingEndedRestoresDefaultContext() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + // LS (the last phase) overridden to a different EnvironmentMode than the default, forcing + // AbstractSolver.preparePhase() to swap in a non-default context for it. + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + SolverFactory solverFactory = SolverFactory.create(solverConfig); + var solver = (AbstractSolver) solverFactory.buildSolver(); + var problem = TestdataSolution.generateSolution(2, 2); + solver.solve(problem); + assertThat(solver.defaultSolverContext.scoreDirector().getWorkingSolution()).isNull(); + } + + @Test + void ensureScoreCalculationCountConsistent() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + // LS (the last phase) overridden to a different EnvironmentMode than the default, forcing + // AbstractSolver.preparePhase() to swap to a non-default context for it, and solvingEnded() to + // restore the (already-populated, since CH ran on it first) default context afterward. + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + SolverFactory solverFactory = SolverFactory.create(solverConfig); + var solver = (AbstractSolver) solverFactory.buildSolver(); + var problem = TestdataSolution.generateSolution(2, 2); + + // Capture the score calculation count after the CH phase and + var calculationCountBeforeRestore = new AtomicLong(-1); + solver.addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter() { + @Override + public void solvingEnded(SolverScope solverScope) { + calculationCountBeforeRestore.set(solverScope.getScoreDirector().getCalculationCount()); + } + }); + solver.solve(problem); + + // After solvingEnded() restores defaultSolverContext, its calculation count must equal exactly + // the true running total captured above + assertThat(solver.defaultSolverContext.scoreDirector().getCalculationCount()) + .isEqualTo(calculationCountBeforeRestore.get()); + } + @Test void solveRepeatedlyBasicVariable(SoftAssertions softly) { var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); @@ -2035,6 +2081,41 @@ void failLocalSearchValueRangeAssertion() { "The value (bad value) from the planning variable (valueList) has been assigned to the entity (Generated Entity 0), but it is outside of the related value range [Generated Value 0-Generated Value 1]"); } + @Test + void solvingErrorRestoresDefaultContextWhenPhaseFails() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, + TestdataListValue.class); + var localSearchPhaseConfig = new LocalSearchPhaseConfig(); + localSearchPhaseConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + // Force invalid move factory + localSearchPhaseConfig.setMoveSelectorConfig( + new MoveIteratorFactoryConfig().withMoveIteratorFactoryClass(InvalidMoveListFactory.class)); + solverConfig.setPhaseConfigList(List.of(new ConstructionHeuristicPhaseConfig(), localSearchPhaseConfig)); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + var solver = (AbstractSolver) solverFactory.buildSolver(); + var problem = TestdataListSolution.generateUninitializedSolution(2, 2); + + // Expected to be the director created for the LS phase + var swappedScoreDirector = new AtomicReference<>(); + solver.addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter<>() { + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + if (phaseScope.getPhaseIndex() == 1) { + swappedScoreDirector.set(phaseScope.getScoreDirector()); + } + } + }); + + assertThatCode(() -> solver.solve(problem)) + .hasMessageContaining("The value (bad value) from the planning variable (valueList)"); + + // The orphaned non-default director must have been closed + assertThat(swappedScoreDirector.get()).isNotNull(); + var closedWorkingSolution = ((InnerScoreDirector) swappedScoreDirector.get()).getWorkingSolution(); + assertThat(closedWorkingSolution).isNull(); + } + @Test void failCustomPhaseValueRangeAssertion() { var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, @@ -2354,6 +2435,103 @@ void solveCorruptedIncrementalInitialized() { .hasMessageContaining("Score corruption analysis:"); } + @Test + void assertDefaultEnvironmentMode() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + var solverFactory = SolverFactory. create(solverConfig); + DefaultSolver solver = (DefaultSolver) solverFactory.buildSolver(); + assertThat(solver.getPhaseList().stream().map(Phase::getEnvironmentMode).toList()) + .containsOnly(EnvironmentMode.PHASE_ASSERT); + } + + @Test + void assertUpdatedDefaultEnvironmentMode() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + solverConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + var solverFactory = SolverFactory. create(solverConfig); + DefaultSolver solver = (DefaultSolver) solverFactory.buildSolver(); + assertThat(solver.getPhaseList().stream().map(Phase::getEnvironmentMode).toList()) + .containsOnly(EnvironmentMode.FULL_ASSERT); + } + + @Test + void assertPhaseEnvironmentMode() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + solverConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + // LS with NO_ASSERT + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.TRACKED_FULL_ASSERT); + var solverFactory = SolverFactory. create(solverConfig); + DefaultSolver solver = (DefaultSolver) solverFactory.buildSolver(); + assertThat(solver.getPhaseList().stream().map(Phase::getEnvironmentMode).toList()) + .containsExactly(EnvironmentMode.FULL_ASSERT, EnvironmentMode.TRACKED_FULL_ASSERT); + } + + @Test + void assertDefaultPhaseEnvironmentMode() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + // LS with FULL_ASSERT + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + var solverFactory = SolverFactory. create(solverConfig); + DefaultSolver solver = (DefaultSolver) solverFactory.buildSolver(); + assertThat(solver.getPhaseList().stream().map(Phase::getEnvironmentMode).toList()) + .containsExactly(EnvironmentMode.PHASE_ASSERT, EnvironmentMode.FULL_ASSERT); + } + + @Test + void solveWithPhaseEnvironmentModeOverride() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + // LS phase overridden to TRACKED_FULL_ASSERT + solverConfig.getPhaseConfigList().get(1).setEnvironmentMode(EnvironmentMode.TRACKED_FULL_ASSERT); + var problem = TestdataSolution.generateSolution(2, 2); + var bestSolution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(bestSolution).isNotNull(); + } + + @Test + void solveListVariableWithPhaseEnvironmentModeOverride() { + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class); + // LS phase overridden to TRACKED_FULL_ASSERT + var localSearchPhaseConfig = solverConfig.getPhaseConfigList().get(1); + localSearchPhaseConfig.setEnvironmentMode(EnvironmentMode.TRACKED_FULL_ASSERT); + localSearchPhaseConfig.setTerminationConfig(new TerminationConfig().withStepCountLimit(50)); + var problem = TestdataListSolution.generateUninitializedSolution(20, 5); + var bestSolution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(bestSolution).isNotNull(); + } + + @Test + void ensureListVariableStateIsReleased() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, + TestdataListValue.class); + var phaseConfigList = new ArrayList<>(solverConfig.getPhaseConfigList()); + phaseConfigList.add(new LocalSearchPhaseConfig().withTerminationConfig(new TerminationConfig().withStepCountLimit(10))); + solverConfig.setPhaseConfigList(phaseConfigList); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + var solver = (AbstractSolver) solverFactory.buildSolver(); + var problem = TestdataListSolution.generateUninitializedSolution(10, 4); + + var listVariableDescriptor = solver.defaultSolverContext.scoreDirector().getSolutionDescriptor() + .findEntityDescriptorOrFail(TestdataListEntity.class) + .getListVariableDescriptor(); + + // Capture the SupplyManager's demand ref count right after each phase ends (CH, LS1, LS2 in order). + var countsAfterEachPhase = new ArrayList(); + solver.addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter() { + @Override + public void phaseEnded(AbstractPhaseScope phaseScope) { + countsAfterEachPhase.add(phaseScope.getScoreDirector().getSupplyManager() + .getActiveCount(listVariableDescriptor.getStateDemand())); + } + }); + solver.solve(problem); + // Three phases: CS, LS1 and LS2 + assertThat(countsAfterEachPhase).hasSize(3); + // The count of demanded list variable state must be equal for both LS phases + assertThat(countsAfterEachPhase.get(2)).isEqualTo(countsAfterEachPhase.get(1)); + } + @NullMarked public static class CorruptedIncrementalScoreCalculator implements AnalyzableIncrementalScoreCalculator { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java index 6cc8d2f0a7f..38b194dab07 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java @@ -3,7 +3,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertCode; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.util.ArrayList; import java.util.Arrays; @@ -55,7 +55,6 @@ import ai.timefold.solver.core.testutil.AbstractMeterTest; import ai.timefold.solver.core.testutil.PlannerTestUtils; -import org.assertj.core.api.Assertions; import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; @@ -136,12 +135,7 @@ void checkDefaultMeters() { latch.countDown(); }); solver.solve(solution); - - try { - latch.await(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Assertions.fail("Failed waiting for the event to happen.", e); - } + assertDoesNotThrow(() -> latch.await(10, TimeUnit.SECONDS), "Failed waiting for the event to happen."); // Score calculation and problem scale counts should be removed // since registering multiple gauges with the same id @@ -231,11 +225,7 @@ void checkDefaultMetersTags() { }); solver.solve(solution); - try { - latch.await(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Assertions.fail("Failed waiting for the event to happen.", e); - } + assertDoesNotThrow(() -> latch.await(10, TimeUnit.SECONDS), "Failed waiting for the event to happen."); // Score calculation and problem scale counts should be removed // since registering multiple gauges with the same id @@ -298,11 +288,7 @@ void solveMetrics() { }); solution = solver.solve(solution); - try { - latch.await(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Assertions.fail("Failed waiting for the event to happen.", e); - } + assertDoesNotThrow(() -> latch.await(10, TimeUnit.SECONDS), "Failed waiting for the event to happen."); meterRegistry.publish(); assertThat(solution).isNotNull(); assertThat(solution.getEntityList().stream() @@ -451,11 +437,7 @@ void solveBestScoreMetrics() { }); solution = solver.solve(solution); - try { - latch.await(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - fail("Failed waiting for the event to happen.", e); - } + assertDoesNotThrow(() -> latch.await(10, TimeUnit.SECONDS), "Failed waiting for the event to happen."); assertThat(step.get()).isEqualTo(2); meterRegistry.publish(); assertThat(solution).isNotNull(); diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc index e570b96242b..d673c1b8f14 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc @@ -210,6 +210,37 @@ If your production environment doesn't care about reproducibility, use this mode Unlike all the other modes, this mode doesn't use any fixed <> unless one is provided. +[#environmentModePerPhase] +=== Using different environment modes per phase + +By default, every phase of the solver - such as xref:optimization-algorithms/construction-heuristics.adoc#constructionHeuristicsOverview[Construction Heuristic] and xref:optimization-algorithms/local-search.adoc#localSearchOverview[Local Search] - runs in the solver's environment mode. +If the solver's environment mode is not explicitly configured, that is the default `<>` mode. + +Each phase can override the solver's environment mode with a stricter mode of its own. +This is useful when you suspect that a bug is introduced during a specific phase: +instead of paying the performance cost of a stricter mode (such as `<>`) for the entire solver, +you enable it for only the phase under suspicion, while the other phases keep running at a faster mode. + +A phase's environment mode must be at least as strict as the solver's environment mode; it can never be less strict. +At least one phase must still use the solver's environment mode as-is. +If the solver's environment mode is `<>`, no phase can override it, +because every other mode is <> and therefore stricter. + +[source,java,options="nowrap"] +---- +SolverConfig solverConfig = new SolverConfig() + ... + .withEnvironmentMode(EnvironmentMode.PHASE_ASSERT) + .withPhases( + new ConstructionHeuristicPhaseConfig(), + new LocalSearchPhaseConfig() + .withEnvironmentMode(EnvironmentMode.FULL_ASSERT)); +---- + +In this example, the solver's environment mode is `PHASE_ASSERT`. +The Construction Heuristic phase has no environment mode of its own, so it uses that default. +The Local Search phase overrides it and runs in the stricter `FULL_ASSERT` mode instead. + [#environmentModeBestPractices] === Best practices diff --git a/tools/benchmark/src/main/resources/benchmark.xsd b/tools/benchmark/src/main/resources/benchmark.xsd index 87ef47a9121..008f2a3fd67 100644 --- a/tools/benchmark/src/main/resources/benchmark.xsd +++ b/tools/benchmark/src/main/resources/benchmark.xsd @@ -728,6 +728,9 @@ + + +