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 8bed6176b3..fbd76c4534 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 @@ -101,7 +101,7 @@ protected ConstructionHeuristicPhaseScope buildPhaseScope(SolverScope return new ConstructionHeuristicPhaseScope<>(solverScope, phaseIndex); } - private void doStep(ConstructionHeuristicStepScope stepScope) { + protected void doStep(ConstructionHeuristicStepScope stepScope) { var step = stepScope.getStep(); step.execute(stepScope.getMoveDirector()); predictWorkingStepScore(stepScope, step); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/AbstractEntityPlacer.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/AbstractEntityPlacer.java index ccef6dca2b..25ce203dff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/AbstractEntityPlacer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/AbstractEntityPlacer.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.constructionheuristic.placer; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; 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; @@ -17,8 +18,24 @@ public abstract class AbstractEntityPlacer implements EntityPlacer factory; + protected final HeuristicConfigPolicy configPolicy; + protected PhaseLifecycleSupport phaseLifecycleSupport = new PhaseLifecycleSupport<>(); + AbstractEntityPlacer(EntityPlacerFactory factory, HeuristicConfigPolicy configPolicy) { + this.factory = factory; + this.configPolicy = configPolicy; + } + + @Override + public EntityPlacer copy() { + if (factory == null || configPolicy == null) { + throw new IllegalStateException("The entity placer cannot be copied."); + } + return factory.buildEntityPlacer(configPolicy.copyConfigPolicy()); + } + @Override public void solvingStarted(SolverScope solverScope) { phaseLifecycleSupport.fireSolvingStarted(solverScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacer.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacer.java index fb9019d57e..b52d0c45d1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacer.java @@ -7,4 +7,5 @@ public interface EntityPlacer extends Iterable>, EntityPlacer rebuildWithFilter(SelectionFilter filter); + EntityPlacer copy(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacer.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacer.java index 05120369ce..382bc07304 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacer.java @@ -2,6 +2,7 @@ import java.util.Iterator; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator; @@ -13,7 +14,9 @@ public class PooledEntityPlacer extends AbstractEntityPlacer moveSelector; - public PooledEntityPlacer(MoveSelector moveSelector) { + public PooledEntityPlacer(EntityPlacerFactory factory, HeuristicConfigPolicy configPolicy, + MoveSelector moveSelector) { + super(factory, configPolicy); this.moveSelector = moveSelector; phaseLifecycleSupport.addEventListener(moveSelector); } @@ -25,7 +28,7 @@ public Iterator> iterator() { @Override public EntityPlacer rebuildWithFilter(SelectionFilter filter) { - return new PooledEntityPlacer<>(FilteringMoveSelector.of(moveSelector, filter::accept)); + return new PooledEntityPlacer<>(factory, configPolicy, FilteringMoveSelector.of(moveSelector, filter::accept)); } private class PooledEntityPlacingIterator extends UpcomingSelectionIterator> { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacerFactory.java index a9c0d4965c..34c9ebbb8f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/PooledEntityPlacerFactory.java @@ -69,7 +69,7 @@ public PooledEntityPlacer buildEntityPlacer(HeuristicConfigPolicy moveSelector = MoveSelectorFactory. create(moveSelectorConfig_) .buildMoveSelector(configPolicy, SelectionCacheType.JUST_IN_TIME, SelectionOrder.ORIGINAL, false); - return new PooledEntityPlacer<>(moveSelector); + return new PooledEntityPlacer<>(this, configPolicy, moveSelector); } private MoveSelectorConfig buildMoveSelectorConfig(HeuristicConfigPolicy configPolicy) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacer.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacer.java index f01e106abb..762e09fa7e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacer.java @@ -4,6 +4,7 @@ import java.util.Iterator; import java.util.List; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator; @@ -17,7 +18,9 @@ public class QueuedEntityPlacer extends AbstractEntityPlacer entitySelector; protected final List> moveSelectorList; - public QueuedEntityPlacer(EntitySelector entitySelector, List> moveSelectorList) { + public QueuedEntityPlacer(EntityPlacerFactory factory, HeuristicConfigPolicy configPolicy, + EntitySelector entitySelector, List> moveSelectorList) { + super(factory, configPolicy); this.entitySelector = entitySelector; this.moveSelectorList = moveSelectorList; phaseLifecycleSupport.addEventListener(entitySelector); @@ -33,7 +36,8 @@ public Iterator> iterator() { @Override public EntityPlacer rebuildWithFilter(SelectionFilter filter) { - return new QueuedEntityPlacer<>(FilteringEntitySelector.of(entitySelector, filter), moveSelectorList); + return new QueuedEntityPlacer<>(factory, configPolicy, FilteringEntitySelector.of(entitySelector, filter), + moveSelectorList); } private class QueuedEntityPlacingIterator extends UpcomingSelectionIterator> { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java index a33271f874..d013a1ec8b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java @@ -76,7 +76,7 @@ public QueuedEntityPlacer buildEntityPlacer(HeuristicConfigPolicy(entitySelector, moveSelectorList); + return new QueuedEntityPlacer<>(this, configPolicy, entitySelector, moveSelectorList); } @SuppressWarnings("rawtypes") diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacer.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacer.java index 8ea05add13..8a5b6ebf3c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacer.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.Iterator; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; @@ -16,8 +17,9 @@ public class QueuedValuePlacer extends AbstractEntityPlacer valueSelector; protected final MoveSelector moveSelector; - public QueuedValuePlacer(EntityIndependentValueSelector valueSelector, - MoveSelector moveSelector) { + public QueuedValuePlacer(EntityPlacerFactory factory, HeuristicConfigPolicy configPolicy, + EntityIndependentValueSelector valueSelector, MoveSelector moveSelector) { + super(factory, configPolicy); this.valueSelector = valueSelector; this.moveSelector = moveSelector; phaseLifecycleSupport.addEventListener(valueSelector); @@ -59,7 +61,7 @@ protected Placement createUpcomingSelection() { @Override public EntityPlacer rebuildWithFilter(SelectionFilter filter) { - return new QueuedValuePlacer<>( + return new QueuedValuePlacer<>(factory, configPolicy, (EntityIndependentFilteringValueSelector) FilteringValueSelector.of(valueSelector, filter), moveSelector); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacerFactory.java index 64777cf8eb..76a6332449 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacerFactory.java @@ -55,7 +55,8 @@ public QueuedValuePlacer buildEntityPlacer(HeuristicConfigPolicy((EntityIndependentValueSelector) valueSelector, moveSelector); + return new QueuedValuePlacer<>(this, configPolicy, (EntityIndependentValueSelector) valueSelector, + moveSelector); } private ValueSelectorConfig buildValueSelectorConfig(HeuristicConfigPolicy configPolicy, 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 6d93b44c3d..f769c37f62 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 @@ -147,6 +147,16 @@ public Builder cloneBuilder() { .withLogIndentation(logIndentation); } + public HeuristicConfigPolicy copyConfigPolicy() { + return cloneBuilder() + .withEntitySorterManner(entitySorterManner) + .withValueSorterManner(valueSorterManner) + .withReinitializeVariableFilterEnabled(reinitializeVariableFilterEnabled) + .withInitializedChainedValueFilterEnabled(initializedChainedValueFilterEnabled) + .withUnassignedValuesAllowed(unassignedValuesAllowed) + .build(); + } + public HeuristicConfigPolicy createPhaseConfigPolicy() { return cloneBuilder().build(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java index 4783079923..1dfc6a22e7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java @@ -1,16 +1,28 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + import ai.timefold.solver.core.impl.constructionheuristic.ConstructionHeuristicPhase; import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; +import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; -final class RuinRecreateConstructionHeuristicPhase +public final class RuinRecreateConstructionHeuristicPhase extends DefaultConstructionHeuristicPhase implements ConstructionHeuristicPhase { + private final Set elementsToRuinSet; + // Store the original value list of elements that are not included in the initial list of ruined elements + private final Map> missingUpdatedElementsMap; + RuinRecreateConstructionHeuristicPhase(RuinRecreateConstructionHeuristicPhaseBuilder builder) { super(builder); + this.elementsToRuinSet = builder.elementsToRuin; + this.missingUpdatedElementsMap = new IdentityHashMap<>(); } @Override @@ -28,4 +40,24 @@ public String getPhaseTypeString() { return "Ruin & Recreate Construction Heuristics"; } + @Override + protected void doStep(ConstructionHeuristicStepScope stepScope) { + if (elementsToRuinSet != null) { + var listVariableDescriptor = stepScope.getPhaseScope().getSolverScope().getSolutionDescriptor() + .getListVariableDescriptor(); + var entity = stepScope.getStep().extractPlanningEntities().iterator().next(); + if (!elementsToRuinSet.contains(entity)) { + // Sometimes, the list of elements to be ruined does not include new destinations selected by the CH. + // In these cases, we need to record the element list before making any move changes + // so that it can be referenced to restore the solution to its original state when undoing changes. + missingUpdatedElementsMap.computeIfAbsent(entity, + e -> List.copyOf(listVariableDescriptor.getValue(e))); + } + } + super.doStep(stepScope); + } + + public Map> getMissingUpdatedElementsMap() { + return missingUpdatedElementsMap; + } } 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 ccb5bbd0f1..5f1c891ae3 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 @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic; import java.util.List; +import java.util.Set; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; @@ -8,6 +9,7 @@ import ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider; import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacer; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.termination.Termination; import ai.timefold.solver.core.impl.solver.termination.TerminationFactory; @@ -24,16 +26,49 @@ public static RuinRecreateConstructionHeuristicPhaseBuilder solverConfigPolicy, ConstructionHeuristicPhaseConfig constructionHeuristicConfig) { var constructionHeuristicPhaseFactory = new RuinRecreateConstructionHeuristicPhaseFactory(constructionHeuristicConfig); - return (RuinRecreateConstructionHeuristicPhaseBuilder) constructionHeuristicPhaseFactory.getBuilder(0, false, + var builder = (RuinRecreateConstructionHeuristicPhaseBuilder) constructionHeuristicPhaseFactory.getBuilder(0, + false, solverConfigPolicy, TerminationFactory. create(new TerminationConfig()) .buildTermination(solverConfigPolicy)); + if (solverConfigPolicy.getMoveThreadCount() != null && solverConfigPolicy.getMoveThreadCount() >= 1) { + builder.multithreaded = true; + } + return builder; } - private List elementsToRecreate; + private final HeuristicConfigPolicy configPolicy; + private final RuinRecreateConstructionHeuristicPhaseFactory constructionHeuristicPhaseFactory; + private final Termination phaseTermination; + + Set elementsToRuin; + List elementsToRecreate; + private boolean multithreaded = false; - RuinRecreateConstructionHeuristicPhaseBuilder(Termination phaseTermination, - EntityPlacer entityPlacer, ConstructionHeuristicDecider decider) { + RuinRecreateConstructionHeuristicPhaseBuilder(HeuristicConfigPolicy configPolicy, + RuinRecreateConstructionHeuristicPhaseFactory constructionHeuristicPhaseFactory, + Termination phaseTermination, EntityPlacer entityPlacer, + ConstructionHeuristicDecider decider) { super(0, false, "", phaseTermination, entityPlacer, decider); + this.configPolicy = configPolicy; + this.constructionHeuristicPhaseFactory = constructionHeuristicPhaseFactory; + this.phaseTermination = phaseTermination; + } + + /** + * In a multithreaded environment, the builder will be shared among all moves and threads. + * Consequently, the list {@code elementsToRecreate} used by {@code getEntityPlacer} or the {@code decider}, + * will be shared between the main and move threads. + * This sharing can lead to race conditions. + * The method creates a new copy of the builder and the decider to avoid race conditions. + */ + public RuinRecreateConstructionHeuristicPhaseBuilder + ensureThreadSafe(InnerScoreDirector scoreDirector) { + if (multithreaded && scoreDirector.isDerived()) { + return new RuinRecreateConstructionHeuristicPhaseBuilder<>(configPolicy, constructionHeuristicPhaseFactory, + phaseTermination, super.getEntityPlacer().copy(), + constructionHeuristicPhaseFactory.buildDecider(configPolicy, phaseTermination)); + } + return this; } public RuinRecreateConstructionHeuristicPhaseBuilder withElementsToRecreate(List elements) { @@ -41,12 +76,18 @@ public RuinRecreateConstructionHeuristicPhaseBuilder withElementsToRe return this; } + public RuinRecreateConstructionHeuristicPhaseBuilder withElementsToRuin(Set elements) { + this.elementsToRuin = elements; + return this; + } + @Override public EntityPlacer getEntityPlacer() { + var placer = super.getEntityPlacer(); if (elementsToRecreate == null || elementsToRecreate.isEmpty()) { - return super.getEntityPlacer(); + return placer; } - return super.getEntityPlacer().rebuildWithFilter((scoreDirector, selection) -> { + return placer.rebuildWithFilter((scoreDirector, selection) -> { for (var element : elementsToRecreate) { if (selection == element) { return true; 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 f09f7d19fe..f4c0772215 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 @@ -23,7 +23,7 @@ protected DefaultConstructionHeuristicPhaseBuilder createBuilder( Termination solverTermination, int phaseIndex, boolean triggerFirstInitializedSolutionEvent, EntityPlacer entityPlacer) { var phaseTermination = new PhaseToSolverTerminationBridge<>(new BasicPlumbingTermination(false)); - return new RuinRecreateConstructionHeuristicPhaseBuilder<>(phaseTermination, entityPlacer, + return new RuinRecreateConstructionHeuristicPhaseBuilder<>(phaseConfigPolicy, this, phaseTermination, entityPlacer, buildDecider(phaseConfigPolicy, phaseTermination)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMove.java index abd97055ba..bfe128e047 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMove.java @@ -10,7 +10,7 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; import ai.timefold.solver.core.impl.heuristic.move.Move; -import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; +import ai.timefold.solver.core.impl.move.director.VariableChangeRecordingScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; public final class RuinRecreateMove extends AbstractMove { @@ -38,21 +38,23 @@ public RuinRecreateMove(GenuineVariableDescriptor genuineVariableDesc protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) { recordedNewValues = new Object[ruinedEntityList.size()]; - var castScoreDirector = (VariableDescriptorAwareScoreDirector) scoreDirector; + var recordingScoreDirector = (VariableChangeRecordingScoreDirector) scoreDirector; for (var ruinedEntity : ruinedEntityList) { - castScoreDirector.beforeVariableChanged(genuineVariableDescriptor, ruinedEntity); + recordingScoreDirector.beforeVariableChanged(genuineVariableDescriptor, ruinedEntity); genuineVariableDescriptor.setValue(ruinedEntity, null); - castScoreDirector.afterVariableChanged(genuineVariableDescriptor, ruinedEntity); + recordingScoreDirector.afterVariableChanged(genuineVariableDescriptor, ruinedEntity); } - castScoreDirector.triggerVariableListeners(); + recordingScoreDirector.triggerVariableListeners(); - var constructionHeuristicPhase = constructionHeuristicPhaseBuilder.withElementsToRecreate(ruinedEntityList) + var constructionHeuristicPhase = constructionHeuristicPhaseBuilder + .ensureThreadSafe(recordingScoreDirector.getBacking()) + .withElementsToRecreate(ruinedEntityList) .build(); constructionHeuristicPhase.setSolver(solverScope.getSolver()); constructionHeuristicPhase.solvingStarted(solverScope); constructionHeuristicPhase.solve(solverScope); constructionHeuristicPhase.solvingEnded(solverScope); - castScoreDirector.triggerVariableListeners(); + recordingScoreDirector.triggerVariableListeners(); for (var i = 0; i < ruinedEntityList.size(); i++) { recordedNewValues[i] = genuineVariableDescriptor.getValue(ruinedEntityList.get(i)); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMove.java index 5aed630aa8..5f0a82d2eb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMove.java @@ -10,27 +10,29 @@ import java.util.TreeSet; import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; import ai.timefold.solver.core.impl.heuristic.move.Move; +import ai.timefold.solver.core.impl.heuristic.selector.move.generic.RuinRecreateConstructionHeuristicPhase; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.RuinRecreateConstructionHeuristicPhaseBuilder; import ai.timefold.solver.core.impl.move.director.VariableChangeRecordingScoreDirector; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.util.CollectionUtils; public final class ListRuinRecreateMove extends AbstractMove { - private final ListVariableStateSupply listVariableStateSupply; + private final ListVariableDescriptor listVariableDescriptor; private final List ruinedValueList; private final Set affectedEntitySet; private final RuinRecreateConstructionHeuristicPhaseBuilder constructionHeuristicPhaseBuilder; private final SolverScope solverScope; private final Map> entityToNewPositionMap; - public ListRuinRecreateMove(ListVariableStateSupply listVariableStateSupply, + public ListRuinRecreateMove(ListVariableDescriptor listVariableDescriptor, RuinRecreateConstructionHeuristicPhaseBuilder constructionHeuristicPhaseBuilder, SolverScope solverScope, List ruinedValueList, Set affectedEntitySet) { - this.listVariableStateSupply = listVariableStateSupply; + this.listVariableDescriptor = listVariableDescriptor; this.constructionHeuristicPhaseBuilder = constructionHeuristicPhaseBuilder; this.solverScope = solverScope; this.ruinedValueList = ruinedValueList; @@ -41,63 +43,106 @@ public ListRuinRecreateMove(ListVariableStateSupply listVariableState @Override protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) { entityToNewPositionMap.clear(); - var entityToOriginalPositionMap = - CollectionUtils.> newIdentityHashMap(affectedEntitySet.size()); - for (var valueToRuin : ruinedValueList) { - var location = listVariableStateSupply.getLocationInList(valueToRuin) - .ensureAssigned(); - entityToOriginalPositionMap.computeIfAbsent(location.entity(), - ignored -> new TreeSet<>()).add(new RuinedLocation(valueToRuin, location.index())); - } + var variableChangeRecordingScoreDirector = (VariableChangeRecordingScoreDirector) scoreDirector; + try (var listVariableStateSupply = variableChangeRecordingScoreDirector.getBacking().getSupplyManager() + .demand(listVariableDescriptor.getStateDemand())) { + var entityToOriginalPositionMap = + CollectionUtils.> newIdentityHashMap(affectedEntitySet.size()); + for (var valueToRuin : ruinedValueList) { + var location = listVariableStateSupply.getLocationInList(valueToRuin) + .ensureAssigned(); + entityToOriginalPositionMap.computeIfAbsent(location.entity(), + ignored -> new TreeSet<>()).add(new RuinedLocation(valueToRuin, location.index())); + } - var listVariableDescriptor = listVariableStateSupply.getSourceVariableDescriptor(); - var recordingScoreDirector = (VariableChangeRecordingScoreDirector) scoreDirector; - var nonRecordingScoreDirector = recordingScoreDirector.getDelegate(); - for (var entry : entityToOriginalPositionMap.entrySet()) { - var entity = entry.getKey(); - var originalPositionSet = entry.getValue(); - - // Only record before(), so we can restore the state. - // The after() is sent straight to the real score director. - recordingScoreDirector.beforeListVariableChanged(listVariableDescriptor, entity, - listVariableDescriptor.getFirstUnpinnedIndex(entity), - listVariableDescriptor.getListSize(entity)); - for (var position : originalPositionSet.descendingSet()) { - recordingScoreDirector.beforeListVariableElementUnassigned(listVariableDescriptor, position.ruinedValue()); - listVariableDescriptor.removeElement(entity, position.index()); - recordingScoreDirector.afterListVariableElementUnassigned(listVariableDescriptor, position.ruinedValue()); + var nonRecordingScoreDirector = variableChangeRecordingScoreDirector.getBacking(); + for (var entry : entityToOriginalPositionMap.entrySet()) { + var entity = entry.getKey(); + var originalPositionSet = entry.getValue(); + + // Only record before(), so we can restore the state. + // The after() is sent straight to the real score director. + variableChangeRecordingScoreDirector.beforeListVariableChanged(listVariableDescriptor, entity, + listVariableDescriptor.getFirstUnpinnedIndex(entity), + listVariableDescriptor.getListSize(entity)); + for (var position : originalPositionSet.descendingSet()) { + variableChangeRecordingScoreDirector.beforeListVariableElementUnassigned(listVariableDescriptor, + position.ruinedValue()); + listVariableDescriptor.removeElement(entity, position.index()); + variableChangeRecordingScoreDirector.afterListVariableElementUnassigned(listVariableDescriptor, + position.ruinedValue()); + } + nonRecordingScoreDirector.afterListVariableChanged(listVariableDescriptor, entity, + listVariableDescriptor.getFirstUnpinnedIndex(entity), + listVariableDescriptor.getListSize(entity)); + } + scoreDirector.triggerVariableListeners(); + + var constructionHeuristicPhase = + (RuinRecreateConstructionHeuristicPhase) constructionHeuristicPhaseBuilder + .ensureThreadSafe(variableChangeRecordingScoreDirector.getBacking()) + .withElementsToRuin(entityToOriginalPositionMap.keySet()) + .withElementsToRecreate(ruinedValueList) + .build(); + + var nestedSolverScope = new SolverScope(); + nestedSolverScope.setSolver(solverScope.getSolver()); + nestedSolverScope.setScoreDirector(variableChangeRecordingScoreDirector.getBacking()); + constructionHeuristicPhase.setSolver(nestedSolverScope.getSolver()); + constructionHeuristicPhase.solvingStarted(nestedSolverScope); + constructionHeuristicPhase.solve(nestedSolverScope); + constructionHeuristicPhase.solvingEnded(nestedSolverScope); + scoreDirector.triggerVariableListeners(); + + var entityToInsertedValuesMap = CollectionUtils.> newIdentityHashMap(0); + for (var entity : entityToOriginalPositionMap.keySet()) { + entityToInsertedValuesMap.put(entity, new ArrayList<>()); } - nonRecordingScoreDirector.afterListVariableChanged(listVariableDescriptor, entity, - listVariableDescriptor.getFirstUnpinnedIndex(entity), - listVariableDescriptor.getListSize(entity)); - } - scoreDirector.triggerVariableListeners(); - - var constructionHeuristicPhase = constructionHeuristicPhaseBuilder.withElementsToRecreate(ruinedValueList) - .build(); - constructionHeuristicPhase.setSolver(solverScope.getSolver()); - constructionHeuristicPhase.solvingStarted(solverScope); - constructionHeuristicPhase.solve(solverScope); - constructionHeuristicPhase.solvingEnded(solverScope); - scoreDirector.triggerVariableListeners(); - - var entityToInsertedValuesMap = CollectionUtils.> newIdentityHashMap(0); - for (var entity : entityToOriginalPositionMap.keySet()) { - entityToInsertedValuesMap.put(entity, new ArrayList<>()); - } - for (var ruinedValue : ruinedValueList) { - var location = listVariableStateSupply.getLocationInList(ruinedValue) - .ensureAssigned(); - entityToNewPositionMap.computeIfAbsent(location.entity(), ignored -> new TreeSet<>()) - .add(new RuinedLocation(ruinedValue, location.index())); - entityToInsertedValuesMap.computeIfAbsent(location.entity(), ignored -> new ArrayList<>()).add(ruinedValue); - } + for (var ruinedValue : ruinedValueList) { + var location = listVariableStateSupply.getLocationInList(ruinedValue) + .ensureAssigned(); + entityToNewPositionMap.computeIfAbsent(location.entity(), ignored -> new TreeSet<>()) + .add(new RuinedLocation(ruinedValue, location.index())); + entityToInsertedValuesMap.computeIfAbsent(location.entity(), ignored -> new ArrayList<>()).add(ruinedValue); + } - for (var entry : entityToInsertedValuesMap.entrySet()) { - recordingScoreDirector.recordListAssignment(listVariableDescriptor, entry.getKey(), entry.getValue()); + var onlyRecordingChangesScoreDirector = variableChangeRecordingScoreDirector.getNonDelegating(); + for (var entry : entityToInsertedValuesMap.entrySet()) { + if (!entityToOriginalPositionMap.containsKey(entry.getKey())) { + // The entity has not been evaluated while creating the entityToOriginalPositionMap, + // meaning it is a new destination entity without a ListVariableBeforeChangeAction + // to restore the original elements. + // We need to ensure the before action is executed in order to restore the original elements. + var originalElementList = + constructionHeuristicPhase.getMissingUpdatedElementsMap().get(entry.getKey()); + var currentElementList = List.copyOf(listVariableDescriptor.getValue(entry.getKey())); + // We need to first update the entity element list before tracking changes + // and set it back to the one from the generated solution + listVariableDescriptor.getValue(entry.getKey()).clear(); + listVariableDescriptor.getValue(entry.getKey()).addAll(originalElementList); + onlyRecordingChangesScoreDirector.beforeListVariableChanged(listVariableDescriptor, entry.getKey(), 0, + originalElementList.size()); + listVariableDescriptor.getValue(entry.getKey()).clear(); + listVariableDescriptor.getValue(entry.getKey()).addAll(currentElementList); + } + // Since the solution was generated through a nested phase, + // all actions taken to produce the solution are not accessible. + // Therefore, we need to replicate all the actions required to generate the solution + // while also allowing for restoring the original state. + for (var element : entry.getValue()) { + onlyRecordingChangesScoreDirector.beforeListVariableElementAssigned(listVariableDescriptor, element); + } + onlyRecordingChangesScoreDirector.afterListVariableChanged(listVariableDescriptor, entry.getKey(), + listVariableDescriptor.getFirstUnpinnedIndex(entry.getKey()), + listVariableDescriptor.getListSize(entry.getKey())); + for (var element : entry.getValue()) { + onlyRecordingChangesScoreDirector.afterListVariableElementAssigned(listVariableDescriptor, element); + } + } + variableChangeRecordingScoreDirector.getBacking().getSupplyManager() + .cancel(listVariableDescriptor.getStateDemand()); } - } @Override @@ -119,7 +164,9 @@ public boolean isMoveDoable(ScoreDirector scoreDirector) { public Move rebase(ScoreDirector destinationScoreDirector) { var rebasedRuinedValueList = AbstractMove.rebaseList(ruinedValueList, destinationScoreDirector); var rebasedAffectedEntitySet = AbstractMove.rebaseSet(affectedEntitySet, destinationScoreDirector); - return new ListRuinRecreateMove<>(listVariableStateSupply, constructionHeuristicPhaseBuilder, solverScope, + var rebasedListVariableDescriptor = ((InnerScoreDirector) destinationScoreDirector) + .getSolutionDescriptor().getListVariableDescriptor(); + return new ListRuinRecreateMove<>(rebasedListVariableDescriptor, constructionHeuristicPhaseBuilder, solverScope, rebasedRuinedValueList, rebasedAffectedEntitySet); } @@ -129,14 +176,14 @@ public boolean equals(Object o) { return true; if (!(o instanceof ListRuinRecreateMove that)) return false; - return Objects.equals(listVariableStateSupply, that.listVariableStateSupply) + return Objects.equals(listVariableDescriptor, that.listVariableDescriptor) && Objects.equals(ruinedValueList, that.ruinedValueList) && Objects.equals(affectedEntitySet, that.affectedEntitySet); } @Override public int hashCode() { - return Objects.hash(listVariableStateSupply, ruinedValueList, affectedEntitySet); + return Objects.hash(listVariableDescriptor, ruinedValueList, affectedEntitySet); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveIterator.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveIterator.java index 73c18a505b..88c50591ae 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveIterator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveIterator.java @@ -68,8 +68,8 @@ protected Move createUpcomingSelection() { } } } - return new ListRuinRecreateMove<>(listVariableStateSupply, constructionHeuristicPhaseBuilder, solverScope, - selectedValueList, affectedEntitySet); + return new ListRuinRecreateMove<>(listVariableStateSupply.getSourceVariableDescriptor(), + constructionHeuristicPhaseBuilder, solverScope, selectedValueList, affectedEntitySet); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java index dacaf49bf1..84e2010cc5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.move.director; +import java.util.List; + import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; import ai.timefold.solver.core.preview.api.domain.metamodel.ElementLocation; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; @@ -21,15 +23,17 @@ public final class EphemeralMoveDirector extends MoveDirector(scoreDirector, false)); } + @SuppressWarnings("unchecked") public Move createUndoMove() { - return new RecordedUndoMove<>(getVariableChangeRecordingScoreDirector().copyVariableChanges()); + var changes = (List>) getVariableChangeRecordingScoreDirector().copyChanges(); + return new RecordedUndoMove<>(changes); } @Override public @NonNull ElementLocation getPositionOf( @NonNull PlanningListVariableMetaModel variableMetaModel, @NonNull Value_ value) { - return getPositionOf(getVariableChangeRecordingScoreDirector().getDelegate(), variableMetaModel, value); + return getPositionOf(getVariableChangeRecordingScoreDirector().getBacking(), variableMetaModel, value); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/RecordedUndoMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/RecordedUndoMove.java index 37d4a9b23f..01e52662af 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/RecordedUndoMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/RecordedUndoMove.java @@ -33,4 +33,7 @@ public void execute(@NonNull MutableSolutionView solutionView) { .toList()); } + List> getVariableChangeActionList() { + return variableChangeActionList; + } } \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java index 9bd6c54e6b..bb17755800 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java @@ -4,6 +4,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -11,13 +12,15 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; -import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; +import ai.timefold.solver.core.impl.score.director.RevertableScoreDirector; import ai.timefold.solver.core.impl.score.director.VariableDescriptorCache; -public final class VariableChangeRecordingScoreDirector implements VariableDescriptorAwareScoreDirector { +public final class VariableChangeRecordingScoreDirector + implements RevertableScoreDirector { - private final InnerScoreDirector delegate; + private final InnerScoreDirector backingScoreDirector; private final List> variableChanges; + /* * The fromIndex of afterListVariableChanged must match the fromIndex of its beforeListVariableChanged call. * Otherwise this will happen in the undo move: @@ -37,22 +40,32 @@ public final class VariableChangeRecordingScoreDirector implements Va */ private final Map cache; - public VariableChangeRecordingScoreDirector(ScoreDirector delegate) { - this(delegate, true); + public VariableChangeRecordingScoreDirector(ScoreDirector backingScoreDirector) { + this(backingScoreDirector, true); } - public VariableChangeRecordingScoreDirector(ScoreDirector delegate, boolean requiresIndexCache) { - this.delegate = (InnerScoreDirector) delegate; + public VariableChangeRecordingScoreDirector(ScoreDirector backingScoreDirector, boolean requiresIndexCache) { + this.backingScoreDirector = (InnerScoreDirector) backingScoreDirector; this.cache = requiresIndexCache ? new IdentityHashMap<>() : null; // Intentional LinkedList; fast clear, no allocations upfront, // will most often only carry a small number of items. this.variableChanges = new LinkedList<>(); } - List> copyVariableChanges() { + private VariableChangeRecordingScoreDirector(InnerScoreDirector backingScoreDirector, + List> variableChanges, Map cache) { + this.backingScoreDirector = backingScoreDirector; + this.variableChanges = variableChanges; + this.cache = cache; + } + + @Override + @SuppressWarnings("unchecked") + public List copyChanges() { return List.copyOf(variableChanges); } + @Override public void undoChanges() { var changeCount = variableChanges.size(); if (changeCount == 0) { @@ -61,9 +74,9 @@ public void undoChanges() { var listIterator = variableChanges.listIterator(changeCount); while (listIterator.hasPrevious()) { // Iterate in reverse. var changeAction = listIterator.previous(); - changeAction.undo(delegate); + changeAction.undo(backingScoreDirector); } - delegate.triggerVariableListeners(); + Objects.requireNonNull(backingScoreDirector).triggerVariableListeners(); variableChanges.clear(); if (cache != null) { cache.clear(); @@ -73,12 +86,16 @@ public void undoChanges() { @Override public void beforeVariableChanged(VariableDescriptor variableDescriptor, Object entity) { variableChanges.add(new VariableChangeAction<>(entity, variableDescriptor.getValue(entity), variableDescriptor)); - delegate.beforeVariableChanged(variableDescriptor, entity); + if (backingScoreDirector != null) { + backingScoreDirector.beforeVariableChanged(variableDescriptor, entity); + } } @Override public void afterVariableChanged(VariableDescriptor variableDescriptor, Object entity) { - delegate.afterVariableChanged(variableDescriptor, entity); + if (backingScoreDirector != null) { + backingScoreDirector.afterVariableChanged(variableDescriptor, entity); + } } @Override @@ -92,7 +109,9 @@ public void beforeListVariableChanged(ListVariableDescriptor variable variableChanges.add(new ListVariableBeforeChangeAction<>(entity, List.copyOf(list.subList(fromIndex, toIndex)), fromIndex, toIndex, variableDescriptor)); - delegate.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + if (backingScoreDirector != null) { + backingScoreDirector.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + } } @Override @@ -109,67 +128,92 @@ The fromIndex of afterListVariableChanged (%d) must match the fromIndex of its b } } variableChanges.add(new ListVariableAfterChangeAction<>(entity, fromIndex, toIndex, variableDescriptor)); - delegate.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + if (backingScoreDirector != null) { + backingScoreDirector.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + } } @Override public void beforeListVariableElementAssigned(ListVariableDescriptor variableDescriptor, Object element) { variableChanges.add(new ListVariableBeforeAssignmentAction<>(element, variableDescriptor)); - delegate.beforeListVariableElementAssigned(variableDescriptor, element); + if (backingScoreDirector != null) { + backingScoreDirector.beforeListVariableElementAssigned(variableDescriptor, element); + } } @Override public void afterListVariableElementAssigned(ListVariableDescriptor variableDescriptor, Object element) { variableChanges.add(new ListVariableAfterAssignmentAction<>(element, variableDescriptor)); - delegate.afterListVariableElementAssigned(variableDescriptor, element); + if (backingScoreDirector != null) { + backingScoreDirector.afterListVariableElementAssigned(variableDescriptor, element); + } } @Override public void beforeListVariableElementUnassigned(ListVariableDescriptor variableDescriptor, Object element) { variableChanges.add(new ListVariableBeforeUnassignmentAction<>(element, variableDescriptor)); - delegate.beforeListVariableElementUnassigned(variableDescriptor, element); + if (backingScoreDirector != null) { + backingScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, element); + } } @Override public void afterListVariableElementUnassigned(ListVariableDescriptor variableDescriptor, Object element) { variableChanges.add(new ListVariableAfterUnassignmentAction<>(element, variableDescriptor)); - delegate.afterListVariableElementUnassigned(variableDescriptor, element); + if (backingScoreDirector != null) { + backingScoreDirector.afterListVariableElementUnassigned(variableDescriptor, element); + } } // For other operations, call the delegate's method. @Override public SolutionDescriptor getSolutionDescriptor() { - return delegate.getSolutionDescriptor(); + return Objects.requireNonNull(backingScoreDirector).getSolutionDescriptor(); + } + + /** + * Returns the score director to which events are delegated. + */ + public InnerScoreDirector getBacking() { + return backingScoreDirector; } - public InnerScoreDirector getDelegate() { - return delegate; + /** + * The {@code VariableChangeRecordingScoreDirector} score director includes two main tasks: + * tracking any variable change and firing events to a delegated score director. + * This method returns a copy of the score director + * that only tracks variable changes without firing any delegated score director events. + */ + public VariableChangeRecordingScoreDirector getNonDelegating() { + return new VariableChangeRecordingScoreDirector<>(null, variableChanges, cache); } @Override public Solution_ getWorkingSolution() { - return delegate.getWorkingSolution(); + return Objects.requireNonNull(backingScoreDirector).getWorkingSolution(); } @Override public VariableDescriptorCache getVariableDescriptorCache() { - return delegate.getVariableDescriptorCache(); + return Objects.requireNonNull(backingScoreDirector).getVariableDescriptorCache(); } @Override public void triggerVariableListeners() { - delegate.triggerVariableListeners(); + if (backingScoreDirector != null) { + backingScoreDirector.triggerVariableListeners(); + } } @Override public E lookUpWorkingObject(E externalObject) { - return delegate.lookUpWorkingObject(externalObject); + return Objects.requireNonNull(backingScoreDirector).lookUpWorkingObject(externalObject); } @Override public E lookUpWorkingObjectOrReturnNull(E externalObject) { - return delegate.lookUpWorkingObjectOrReturnNull(externalObject); + return Objects.requireNonNull(backingScoreDirector).lookUpWorkingObjectOrReturnNull(externalObject); } @Override @@ -178,29 +222,4 @@ public void changeVariableFacade(VariableDescriptor variableDescripto variableDescriptor.setValue(entity, newValue); afterVariableChanged(variableDescriptor, entity); } - - /** - * Record a list assignment. - * Used by moves with nested phases, such as ruin and recreate. - * These moves need to record some changes manually, - * because they cannot influence what will happen in the nested phases. - * Cannot be sent via the before/after events, because we don't want to delegate to the actual score director; - * these new changes should only be used for undoing the move(s). - * - * @param variableDescriptor never null - * @param entity never null - * @param values never null, may be empty - */ - public void recordListAssignment(ListVariableDescriptor variableDescriptor, Object entity, - List values) { - for (var element : values) { - variableChanges.add(new ListVariableBeforeAssignmentAction<>(element, variableDescriptor)); - } - variableChanges.add(new ListVariableAfterChangeAction<>(entity, - variableDescriptor.getFirstUnpinnedIndex(entity), variableDescriptor.getListSize(entity), - variableDescriptor)); - for (var element : values) { - variableChanges.add(new ListVariableAfterAssignmentAction<>(element, variableDescriptor)); - } - } } 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 e86d8409ac..fcfe6454a6 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 @@ -338,6 +338,14 @@ default Solution_ cloneWorkingSolution() { */ void forceTriggerVariableListeners(); + /** + * A derived score director is created from a root score director. + * The derived score director can be used to create separate* instances for use cases like multithreaded solving. + */ + default boolean isDerived() { + return false; + } + default ScoreAnalysis buildScoreAnalysis(ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { return buildScoreAnalysis(scoreAnalysisFetchPolicy, ScoreAnalysisMode.DEFAULT); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java new file mode 100644 index 0000000000..1a319ad911 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java @@ -0,0 +1,20 @@ +package ai.timefold.solver.core.impl.score.director; + +import java.util.List; + +public interface RevertableScoreDirector extends VariableDescriptorAwareScoreDirector { + + /** + * Use this method to get a copy of all non-commited changes executed by the director so far. + * + * @param The action type for recorded changes + */ + List copyChanges(); + + /** + * Use this method to revert all changes made by moves. + * The score director that implements this logic must be able to track every single change in the solution and + * restore it to its original state. + */ + void undoChanges(); +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java index 7cfa71b508..0395aefb96 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java @@ -177,4 +177,8 @@ public BavetConstraintSession getSession() { return session; } + @Override + public boolean isDerived() { + return derived; + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/PooledEntityPlacerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/PooledEntityPlacerTest.java index ca997bb7dc..bb77528246 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/PooledEntityPlacerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/PooledEntityPlacerTest.java @@ -3,16 +3,21 @@ import static ai.timefold.solver.core.impl.testdata.util.PlannerAssert.assertAllCodesOfIterator; import static ai.timefold.solver.core.impl.testdata.util.PlannerAssert.verifyPhaseLifecycle; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Iterator; +import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacerFactory; import ai.timefold.solver.core.impl.constructionheuristic.placer.Placement; import ai.timefold.solver.core.impl.constructionheuristic.placer.PooledEntityPlacer; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.move.DummyMove; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; -import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -24,21 +29,21 @@ class PooledEntityPlacerTest { @Test void oneMoveSelector() { - MoveSelector moveSelector = SelectorTestUtils.mockMoveSelector( + var moveSelector = SelectorTestUtils.mockMoveSelector( new DummyMove("a1"), new DummyMove("a2"), new DummyMove("b1")); - PooledEntityPlacer placer = new PooledEntityPlacer<>(moveSelector); + var placer = new PooledEntityPlacer<>(null, null, moveSelector); - SolverScope solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA1 = mock(AbstractStepScope.class); + var stepScopeA1 = mock(AbstractStepScope.class); when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA1); assertAllCodesOfIterator(placementIterator.next().iterator(), @@ -46,7 +51,7 @@ void oneMoveSelector() { placer.stepEnded(stepScopeA1); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA2 = mock(AbstractStepScope.class); + var stepScopeA2 = mock(AbstractStepScope.class); when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA2); assertAllCodesOfIterator(placementIterator.next().iterator(), @@ -54,7 +59,7 @@ void oneMoveSelector() { placer.stepEnded(stepScopeA2); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA3 = mock(AbstractStepScope.class); + var stepScopeA3 = mock(AbstractStepScope.class); when(stepScopeA3.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA3); assertAllCodesOfIterator(placementIterator.next().iterator(), @@ -63,13 +68,13 @@ void oneMoveSelector() { placer.phaseEnded(phaseScopeA); - AbstractPhaseScope phaseScopeB = mock(AbstractPhaseScope.class); + var phaseScopeB = mock(AbstractPhaseScope.class); when(phaseScopeB.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeB); placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeB1 = mock(AbstractStepScope.class); + var stepScopeB1 = mock(AbstractStepScope.class); when(stepScopeB1.getPhaseScope()).thenReturn(phaseScopeB); placer.stepStarted(stepScopeB1); assertAllCodesOfIterator(placementIterator.next().iterator(), @@ -83,4 +88,17 @@ void oneMoveSelector() { verifyPhaseLifecycle(moveSelector, 1, 2, 4); } + @Test + void copy() { + var moveSelector = SelectorTestUtils + .mockMoveSelector(new DummyMove("a1"), new DummyMove("a2"), new DummyMove("b1")); + var factory = mock(EntityPlacerFactory.class); + var configPolicy = mock(HeuristicConfigPolicy.class); + assertThatThrownBy(() -> new PooledEntityPlacer<>(null, null, moveSelector).copy()) + .hasMessage("The entity placer cannot be copied."); + var placer = new PooledEntityPlacer(factory, configPolicy, moveSelector); + placer.copy(); + verify(factory, times(1)).buildEntityPlacer(any()); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java index 199e47156c..6e6ff39b3d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java @@ -4,16 +4,21 @@ import static ai.timefold.solver.core.impl.testdata.util.PlannerAssert.assertAllCodesOfIterator; import static ai.timefold.solver.core.impl.testdata.util.PlannerAssert.verifyPhaseLifecycle; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; -import java.util.List; +import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacerFactory; import ai.timefold.solver.core.impl.constructionheuristic.placer.Placement; import ai.timefold.solver.core.impl.constructionheuristic.placer.QueuedEntityPlacer; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.entity.mimic.MimicRecordingEntitySelector; @@ -44,35 +49,34 @@ void oneMoveSelector() { ValueSelector valueSelector = SelectorTestUtils.mockValueSelector(TestdataEntity.class, "value", new TestdataValue("1"), new TestdataValue("2")); - MoveSelector moveSelector = + var moveSelector = new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), valueSelector, false); - QueuedEntityPlacer placer = - new QueuedEntityPlacer<>(recordingEntitySelector, Collections.singletonList(moveSelector)); + var placer = new QueuedEntityPlacer<>(null, null, recordingEntitySelector, Collections.singletonList(moveSelector)); - SolverScope solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); - Iterator> placementIterator = placer.iterator(); + var placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA1 = mock(AbstractStepScope.class); + var stepScopeA1 = mock(AbstractStepScope.class); when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA1); assertEntityPlacement(placementIterator.next(), "a", "1", "2"); placer.stepEnded(stepScopeA1); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA2 = mock(AbstractStepScope.class); + var stepScopeA2 = mock(AbstractStepScope.class); when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA2); assertEntityPlacement(placementIterator.next(), "b", "1", "2"); placer.stepEnded(stepScopeA2); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA3 = mock(AbstractStepScope.class); + var stepScopeA3 = mock(AbstractStepScope.class); when(stepScopeA3.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA3); assertEntityPlacement(placementIterator.next(), "c", "1", "2"); @@ -81,13 +85,13 @@ void oneMoveSelector() { assertThat(placementIterator).isExhausted(); placer.phaseEnded(phaseScopeA); - AbstractPhaseScope phaseScopeB = mock(AbstractPhaseScope.class); + var phaseScopeB = mock(AbstractPhaseScope.class); when(phaseScopeB.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeB); placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeB1 = mock(AbstractStepScope.class); + var stepScopeB1 = mock(AbstractStepScope.class); when(stepScopeB1.getPhaseScope()).thenReturn(phaseScopeB); placer.stepStarted(stepScopeB1); assertEntityPlacement(placementIterator.next(), "a", "1", "2"); @@ -115,7 +119,7 @@ void multiQueuedMoveSelector() { TestdataMultiVarEntity.class, "secondaryValue", new TestdataValue("8"), new TestdataValue("9")); - List> moveSelectorList = new ArrayList<>(2); + var moveSelectorList = new ArrayList>(2); moveSelectorList.add(new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), primaryValueSelector, false)); @@ -123,40 +127,40 @@ void multiQueuedMoveSelector() { new MimicReplayingEntitySelector<>(recordingEntitySelector), secondaryValueSelector, false)); - QueuedEntityPlacer placer = - new QueuedEntityPlacer<>(recordingEntitySelector, moveSelectorList); + var placer = + new QueuedEntityPlacer<>(null, null, recordingEntitySelector, moveSelectorList); - SolverScope solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA1 = mock(AbstractStepScope.class); + var stepScopeA1 = mock(AbstractStepScope.class); when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA1); assertEntityPlacement(placementIterator.next(), "a", "1", "2", "3"); placer.stepEnded(stepScopeA1); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA2 = mock(AbstractStepScope.class); + var stepScopeA2 = mock(AbstractStepScope.class); when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA2); assertEntityPlacement(placementIterator.next(), "a", "8", "9"); placer.stepEnded(stepScopeA2); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA3 = mock(AbstractStepScope.class); + var stepScopeA3 = mock(AbstractStepScope.class); when(stepScopeA3.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA3); assertEntityPlacement(placementIterator.next(), "b", "1", "2", "3"); placer.stepEnded(stepScopeA3); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA4 = mock(AbstractStepScope.class); + var stepScopeA4 = mock(AbstractStepScope.class); when(stepScopeA4.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA4); assertEntityPlacement(placementIterator.next(), "b", "8", "9"); @@ -186,27 +190,27 @@ void cartesianProductMoveSelector() { TestdataMultiVarEntity.class, "secondaryValue", new TestdataValue("8"), new TestdataValue("9")); - List> moveSelectorList = new ArrayList<>(2); + var moveSelectorList = new ArrayList>(2); moveSelectorList.add(new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), primaryValueSelector, false)); moveSelectorList.add(new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), secondaryValueSelector, false)); - MoveSelector moveSelector = new CartesianProductMoveSelector<>(moveSelectorList, true, false); - QueuedEntityPlacer placer = new QueuedEntityPlacer<>(recordingEntitySelector, + var moveSelector = new CartesianProductMoveSelector<>(moveSelectorList, true, false); + var placer = new QueuedEntityPlacer<>(null, null, recordingEntitySelector, Collections.singletonList(moveSelector)); - SolverScope solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA1 = mock(AbstractStepScope.class); + var stepScopeA1 = mock(AbstractStepScope.class); when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA1); assertAllCodesOfIterator(placementIterator.next().iterator(), @@ -214,7 +218,7 @@ void cartesianProductMoveSelector() { placer.stepEnded(stepScopeA1); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA2 = mock(AbstractStepScope.class); + var stepScopeA2 = mock(AbstractStepScope.class); when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA2); assertAllCodesOfIterator(placementIterator.next().iterator(), @@ -231,4 +235,38 @@ void cartesianProductMoveSelector() { verifyPhaseLifecycle(secondaryValueSelector, 1, 1, 2); } + @Test + void copy() { + EntitySelector entitySelector = + SelectorTestUtils.mockEntitySelector(TestdataMultiVarEntity.class, + new TestdataMultiVarEntity("a"), new TestdataMultiVarEntity("b")); + MimicRecordingEntitySelector recordingEntitySelector = + new MimicRecordingEntitySelector<>(entitySelector); + ValueSelector primaryValueSelector = SelectorTestUtils.mockValueSelector( + TestdataMultiVarEntity.class, "primaryValue", + new TestdataValue("1"), new TestdataValue("2"), new TestdataValue("3")); + ValueSelector secondaryValueSelector = SelectorTestUtils.mockValueSelector( + TestdataMultiVarEntity.class, "secondaryValue", + new TestdataValue("8"), new TestdataValue("9")); + var moveSelectorList = new ArrayList>(2); + moveSelectorList.add(new ChangeMoveSelector<>( + new MimicReplayingEntitySelector<>(recordingEntitySelector), + primaryValueSelector, + false)); + moveSelectorList.add( + new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), secondaryValueSelector, + false)); + var moveSelector = new CartesianProductMoveSelector<>(moveSelectorList, true, false); + var factory = mock(EntityPlacerFactory.class); + var configPolicy = mock(HeuristicConfigPolicy.class); + assertThatThrownBy( + () -> new QueuedEntityPlacer<>(null, null, recordingEntitySelector, Collections.singletonList(moveSelector)) + .copy()) + .hasMessage("The entity placer cannot be copied."); + var placer = new QueuedEntityPlacer(factory, configPolicy, recordingEntitySelector, + Collections.singletonList(moveSelector)); + placer.copy(); + verify(factory, times(1)).buildEntityPlacer(any()); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedValuePlacerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedValuePlacerTest.java index 8cf480ab88..ee7cd26bca 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedValuePlacerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedValuePlacerTest.java @@ -3,17 +3,21 @@ import static ai.timefold.solver.core.impl.constructionheuristic.placer.entity.PlacementAssertions.assertValuePlacement; import static ai.timefold.solver.core.impl.testdata.util.PlannerAssert.verifyPhaseLifecycle; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Iterator; +import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacerFactory; import ai.timefold.solver.core.impl.constructionheuristic.placer.Placement; import ai.timefold.solver.core.impl.constructionheuristic.placer.QueuedValuePlacer; -import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; -import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.ChangeMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.EntityIndependentValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.mimic.MimicRecordingValueSelector; @@ -31,7 +35,7 @@ class QueuedValuePlacerTest { @Test void oneMoveSelector() { - GenuineVariableDescriptor variableDescriptor = TestdataEntity.buildVariableDescriptorForValue(); + var variableDescriptor = TestdataEntity.buildVariableDescriptorForValue(); EntitySelector entitySelector = SelectorTestUtils.mockEntitySelector(variableDescriptor.getEntityDescriptor(), new TestdataEntity("a"), new TestdataEntity("b"), new TestdataEntity("c")); @@ -41,34 +45,34 @@ void oneMoveSelector() { MimicRecordingValueSelector recordingValueSelector = new MimicRecordingValueSelector<>(valueSelector); - MoveSelector moveSelector = new ChangeMoveSelector<>(entitySelector, + var moveSelector = new ChangeMoveSelector<>(entitySelector, new MimicReplayingValueSelector<>(recordingValueSelector), false); - QueuedValuePlacer placer = new QueuedValuePlacer<>(recordingValueSelector, moveSelector); + var placer = new QueuedValuePlacer<>(null, null, recordingValueSelector, moveSelector); - SolverScope solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA1 = mock(AbstractStepScope.class); + var stepScopeA1 = mock(AbstractStepScope.class); when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA1); assertValuePlacement(placementIterator.next(), "1", "a", "b", "c"); placer.stepEnded(stepScopeA1); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA2 = mock(AbstractStepScope.class); + var stepScopeA2 = mock(AbstractStepScope.class); when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA2); assertValuePlacement(placementIterator.next(), "2", "a", "b", "c"); placer.stepEnded(stepScopeA2); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeA3 = mock(AbstractStepScope.class); + var stepScopeA3 = mock(AbstractStepScope.class); when(stepScopeA3.getPhaseScope()).thenReturn(phaseScopeA); placer.stepStarted(stepScopeA3); assertValuePlacement(placementIterator.next(), "1", "a", "b", "c"); @@ -78,13 +82,13 @@ void oneMoveSelector() { // assertFalse(placementIterator.hasNext()); placer.phaseEnded(phaseScopeA); - AbstractPhaseScope phaseScopeB = mock(AbstractPhaseScope.class); + var phaseScopeB = mock(AbstractPhaseScope.class); when(phaseScopeB.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeB); placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope stepScopeB1 = mock(AbstractStepScope.class); + var stepScopeB1 = mock(AbstractStepScope.class); when(stepScopeB1.getPhaseScope()).thenReturn(phaseScopeB); placer.stepStarted(stepScopeB1); assertValuePlacement(placementIterator.next(), "1", "a", "b", "c"); @@ -98,4 +102,27 @@ void oneMoveSelector() { verifyPhaseLifecycle(valueSelector, 1, 2, 4); } + @Test + void copy() { + var variableDescriptor = TestdataEntity.buildVariableDescriptorForValue(); + EntitySelector entitySelector = + SelectorTestUtils.mockEntitySelector(variableDescriptor.getEntityDescriptor(), + new TestdataEntity("a"), new TestdataEntity("b"), new TestdataEntity("c")); + EntityIndependentValueSelector valueSelector = SelectorTestUtils.mockEntityIndependentValueSelector( + variableDescriptor, + new TestdataValue("1"), new TestdataValue("2")); + MimicRecordingValueSelector recordingValueSelector = + new MimicRecordingValueSelector<>(valueSelector); + + var moveSelector = new ChangeMoveSelector<>(entitySelector, + new MimicReplayingValueSelector<>(recordingValueSelector), false); + var factory = mock(EntityPlacerFactory.class); + var configPolicy = mock(HeuristicConfigPolicy.class); + assertThatThrownBy(() -> new QueuedValuePlacer<>(null, null, recordingValueSelector, moveSelector).copy()) + .hasMessage("The entity placer cannot be copied."); + var placer = new QueuedValuePlacer(factory, configPolicy, recordingValueSelector, moveSelector); + placer.copy(); + verify(factory, times(1)).buildEntityPlacer(any()); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilderTest.java new file mode 100644 index 0000000000..e197dbeaa9 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilderTest.java @@ -0,0 +1,48 @@ +package ai.timefold.solver.core.impl.heuristic.selector.move.generic; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; +import ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; +import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; + +import org.junit.jupiter.api.Test; + +class RuinRecreateConstructionHeuristicPhaseBuilderTest { + + @Test + void buildSingleThreaded() { + var solverConfigPolicy = new HeuristicConfigPolicy.Builder() + .withSolutionDescriptor(TestdataSolution.buildSolutionDescriptor()) + .withInitializingScoreTrend(new InitializingScoreTrend(new InitializingScoreTrendLevel[] { + InitializingScoreTrendLevel.ANY, InitializingScoreTrendLevel.ANY, InitializingScoreTrendLevel.ANY })) + .build(); + var constructionHeuristicConfig = mock(ConstructionHeuristicPhaseConfig.class); + var builder = RuinRecreateConstructionHeuristicPhaseBuilder.create(solverConfigPolicy, constructionHeuristicConfig); + var phase = builder.build(); + assertThat(phase.getEntityPlacer()).isSameAs(builder.getEntityPlacer()); + } + + @Test + void buildMultiThreaded() { + var solverConfigPolicy = new HeuristicConfigPolicy.Builder() + .withSolutionDescriptor(TestdataSolution.buildSolutionDescriptor()) + .withMoveThreadCount(2) + .withInitializingScoreTrend(new InitializingScoreTrend(new InitializingScoreTrendLevel[] { + InitializingScoreTrendLevel.ANY, InitializingScoreTrendLevel.ANY, InitializingScoreTrendLevel.ANY })) + .build(); + var constructionHeuristicConfig = mock(ConstructionHeuristicPhaseConfig.class); + var builder = RuinRecreateConstructionHeuristicPhaseBuilder.create(solverConfigPolicy, constructionHeuristicConfig); + var scoreDirector = mock(InnerScoreDirector.class); + when(scoreDirector.isDerived()).thenReturn(true); + var phase = builder + .ensureThreadSafe(scoreDirector) + .build(); + assertThat(phase.getEntityPlacer()).isNotSameAs(builder.getEntityPlacer()); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java index 132600ecec..0b88ac7033 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java @@ -10,7 +10,7 @@ import java.util.List; import java.util.Set; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.RuinRecreateConstructionHeuristicPhaseBuilder; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ruin.ListRuinRecreateMove; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -48,7 +48,7 @@ void rebase() { { e3, destinationE3 }, }); - var move = new ListRuinRecreateMove(mock(ListVariableStateSupply.class), + var move = new ListRuinRecreateMove(mock(ListVariableDescriptor.class), mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), Arrays.asList(v1, v2), Set.of(e1, e2, e3)); var rebasedMove = move.rebase(destinationScoreDirector); @@ -70,26 +70,26 @@ void equality() { var e1 = new TestdataListEntity("e1", v1); var e2 = new TestdataListEntity("e2"); - var supply = mock(ListVariableStateSupply.class); - var move = new ListRuinRecreateMove(supply, + var descriptor = mock(ListVariableDescriptor.class); + var move = new ListRuinRecreateMove(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), Set.of(v1)); - var sameMove = new ListRuinRecreateMove(supply, + var sameMove = new ListRuinRecreateMove(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), Set.of(v1)); assertThat(move).isEqualTo(sameMove); - var differentMove = new ListRuinRecreateMove(supply, + var differentMove = new ListRuinRecreateMove(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), Set.of(v2)); assertThat(move).isNotEqualTo(differentMove); - var anotherDifferentMove = new ListRuinRecreateMove(supply, + var anotherDifferentMove = new ListRuinRecreateMove(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e2), Set.of(v1)); assertThat(move).isNotEqualTo(anotherDifferentMove); - var yetAnotherDifferentMove = new ListRuinRecreateMove(mock(ListVariableStateSupply.class), + var yetAnotherDifferentMove = new ListRuinRecreateMove(mock(ListVariableDescriptor.class), mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), Set.of(v1)); assertThat(move).isNotEqualTo(yetAnotherDifferentMove); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java index 3aad18a692..5fc485843c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java @@ -8,10 +8,21 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +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.heuristic.selector.move.generic.RuinRecreateConstructionHeuristicPhase; +import ai.timefold.solver.core.impl.heuristic.selector.move.generic.RuinRecreateConstructionHeuristicPhaseBuilder; +import ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ruin.ListRuinRecreateMove; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; @@ -203,4 +214,75 @@ void updateShadowVariables() { verify(mockScoreDirector).triggerVariableListeners(); } + @Test + void undoNestedPhaseMove() { + var innerScoreDirector = mock(InnerScoreDirector.class); + var moveDirector = new MoveDirector(innerScoreDirector); + var listVariableStateSupply = mock(ListVariableStateSupply.class); + var listVariableDescriptor = mock(ListVariableDescriptor.class); + var supplyManager = mock(SupplyManager.class); + var ruinRecreateConstructionHeuristicPhaseBuilder = mock(RuinRecreateConstructionHeuristicPhaseBuilder.class); + var constructionHeuristicPhase = mock(RuinRecreateConstructionHeuristicPhase.class); + + // The objective is to simulate the reassignment of v1 from e1 to e2 + // The R&R move analyzes only e1 initially, + // since it is impossible to know that v1 will be assigned to e2 during the nested CH phase + var v1 = new TestdataListValue("v1"); + var v2 = new TestdataListValue("v2"); + var e1 = new TestdataListEntity("e1", v1); + var e2 = new TestdataListEntity("e2", v2, v1); + var s1 = new TestdataListSolution(); + s1.setEntityList(List.of(e1, e2)); + s1.setValueList(List.of(v1, v2)); + when(innerScoreDirector.getWorkingSolution()).thenReturn(s1); + when(innerScoreDirector.isDerived()).thenReturn(false); + when(innerScoreDirector.getSupplyManager()).thenReturn(supplyManager); + when(supplyManager.demand(any())).thenReturn(listVariableStateSupply); + // 1 - v1 is on e1 list + // 2 - v1 moves to e2 list + when(listVariableStateSupply.getLocationInList(any())) + .thenReturn(ElementLocation.of(e1, 0), ElementLocation.of(e2, 1)); + when(listVariableStateSupply.getSourceVariableDescriptor()).thenReturn(listVariableDescriptor); + when(listVariableDescriptor.getFirstUnpinnedIndex(any())).thenReturn(0); + when(listVariableDescriptor.getListSize(any())).thenReturn(1); + when(listVariableDescriptor.getValue(any())).thenReturn(e1.getValueList(), e2.getValueList()); + // Ignore the nested phase but simulates v1 moving to e2 + when(ruinRecreateConstructionHeuristicPhaseBuilder.withElementsToRecreate(any())) + .thenReturn(ruinRecreateConstructionHeuristicPhaseBuilder); + when(ruinRecreateConstructionHeuristicPhaseBuilder.withElementsToRuin(any())) + .thenReturn(ruinRecreateConstructionHeuristicPhaseBuilder); + when(ruinRecreateConstructionHeuristicPhaseBuilder.ensureThreadSafe(any())) + .thenReturn(ruinRecreateConstructionHeuristicPhaseBuilder); + when(ruinRecreateConstructionHeuristicPhaseBuilder.build()) + .thenReturn(constructionHeuristicPhase); + when(constructionHeuristicPhase.getMissingUpdatedElementsMap()).thenReturn(Map.of(e2, List.of(v2))); + + try (var ephemeralMoveDirector = moveDirector.ephemeral()) { + var scoreDirector = ephemeralMoveDirector.getScoreDirector(); + var move = new ListRuinRecreateMove(listVariableDescriptor, + ruinRecreateConstructionHeuristicPhaseBuilder, mock(SolverScope.class), Arrays.asList(v1), Set.of(e1)); + move.doMoveOnly(scoreDirector); + var undoMove = (RecordedUndoMove) ephemeralMoveDirector.createUndoMove(); + // e1 must be analyzed at the beginning of the move execution + assertThat(undoMove.getVariableChangeActionList().stream().anyMatch(action -> { + if (action instanceof ListVariableBeforeChangeAction beforeChangeAction) { + return beforeChangeAction.entity() == e1 && beforeChangeAction.fromIndex() == 0 + && beforeChangeAction.toIndex() == 1 && beforeChangeAction.oldValue().size() == 1 + && beforeChangeAction.oldValue().get(0).equals(v1); + } + return false; + })).isTrue(); + // e2 is not analyzed at the beginning of move execution, + // but it must have a before list change event to restore the original elements. + assertThat(undoMove.getVariableChangeActionList().stream().anyMatch(action -> { + if (action instanceof ListVariableBeforeChangeAction beforeChangeAction) { + return beforeChangeAction.entity() == e2 && beforeChangeAction.fromIndex() == 0 + && beforeChangeAction.toIndex() == 1 && beforeChangeAction.oldValue().size() == 1 + && beforeChangeAction.oldValue().get(0).equals(v2); + } + return false; + })).isTrue(); + } + } + }