From 3247e86046612ef609ea77b08efa532464b66690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederico=20Gon=C3=A7alves?= <zepfred@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:59:12 -0300 Subject: [PATCH] fix: Ruin&Recreate score corruption and multi-threaded fixes (#1320) Fixes TimefoldAI/timefold-employee-scheduling#305 --- .../DefaultConstructionHeuristicPhase.java | 2 +- .../placer/AbstractEntityPlacer.java | 17 ++ .../placer/EntityPlacer.java | 1 + .../placer/PooledEntityPlacer.java | 7 +- .../placer/PooledEntityPlacerFactory.java | 2 +- .../placer/QueuedEntityPlacer.java | 8 +- .../placer/QueuedEntityPlacerFactory.java | 2 +- .../placer/QueuedValuePlacer.java | 8 +- .../placer/QueuedValuePlacerFactory.java | 3 +- .../impl/heuristic/HeuristicConfigPolicy.java | 10 ++ ...uinRecreateConstructionHeuristicPhase.java | 34 +++- ...eateConstructionHeuristicPhaseBuilder.java | 53 +++++- ...eateConstructionHeuristicPhaseFactory.java | 2 +- .../move/generic/RuinRecreateMove.java | 16 +- .../list/ruin/ListRuinRecreateMove.java | 165 +++++++++++------- .../ruin/ListRuinRecreateMoveIterator.java | 4 +- .../move/director/EphemeralMoveDirector.java | 8 +- .../impl/move/director/RecordedUndoMove.java | 3 + .../VariableChangeRecordingScoreDirector.java | 121 +++++++------ .../score/director/InnerScoreDirector.java | 8 + .../director/RevertableScoreDirector.java | 20 +++ .../BavetConstraintStreamScoreDirector.java | 4 + .../placer/entity/PooledEntityPlacerTest.java | 38 ++-- .../placer/entity/QueuedEntityPlacerTest.java | 94 +++++++--- .../placer/entity/QueuedValuePlacerTest.java | 51 ++++-- ...ConstructionHeuristicPhaseBuilderTest.java | 48 +++++ .../list/ListRuinRecreateMoveTest.java | 16 +- .../impl/move/director/MoveDirectorTest.java | 82 +++++++++ 28 files changed, 629 insertions(+), 198 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilderTest.java 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<Solution_> buildPhaseScope(SolverScope return new ConstructionHeuristicPhaseScope<>(solverScope, phaseIndex); } - private void doStep(ConstructionHeuristicStepScope<Solution_> stepScope) { + protected void doStep(ConstructionHeuristicStepScope<Solution_> 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<Solution_> implements EntityPlacer<So protected final transient Logger logger = LoggerFactory.getLogger(getClass()); + protected final EntityPlacerFactory<Solution_> factory; + protected final HeuristicConfigPolicy<Solution_> configPolicy; + protected PhaseLifecycleSupport<Solution_> phaseLifecycleSupport = new PhaseLifecycleSupport<>(); + AbstractEntityPlacer(EntityPlacerFactory<Solution_> factory, HeuristicConfigPolicy<Solution_> configPolicy) { + this.factory = factory; + this.configPolicy = configPolicy; + } + + @Override + public EntityPlacer<Solution_> 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<Solution_> 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<Solution_> extends Iterable<Placement<Solution_>>, EntityPlacer<Solution_> rebuildWithFilter(SelectionFilter<Solution_, Object> filter); + EntityPlacer<Solution_> 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<Solution_> extends AbstractEntityPlacer<Solution protected final MoveSelector<Solution_> moveSelector; - public PooledEntityPlacer(MoveSelector<Solution_> moveSelector) { + public PooledEntityPlacer(EntityPlacerFactory<Solution_> factory, HeuristicConfigPolicy<Solution_> configPolicy, + MoveSelector<Solution_> moveSelector) { + super(factory, configPolicy); this.moveSelector = moveSelector; phaseLifecycleSupport.addEventListener(moveSelector); } @@ -25,7 +28,7 @@ public Iterator<Placement<Solution_>> iterator() { @Override public EntityPlacer<Solution_> rebuildWithFilter(SelectionFilter<Solution_, Object> filter) { - return new PooledEntityPlacer<>(FilteringMoveSelector.of(moveSelector, filter::accept)); + return new PooledEntityPlacer<>(factory, configPolicy, FilteringMoveSelector.of(moveSelector, filter::accept)); } private class PooledEntityPlacingIterator extends UpcomingSelectionIterator<Placement<Solution_>> { 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<Solution_> buildEntityPlacer(HeuristicConfigPolicy<Sol MoveSelector<Solution_> moveSelector = MoveSelectorFactory.<Solution_> 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<Solution_> 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<Solution_> extends AbstractEntityPlacer<Solution protected final EntitySelector<Solution_> entitySelector; protected final List<MoveSelector<Solution_>> moveSelectorList; - public QueuedEntityPlacer(EntitySelector<Solution_> entitySelector, List<MoveSelector<Solution_>> moveSelectorList) { + public QueuedEntityPlacer(EntityPlacerFactory<Solution_> factory, HeuristicConfigPolicy<Solution_> configPolicy, + EntitySelector<Solution_> entitySelector, List<MoveSelector<Solution_>> moveSelectorList) { + super(factory, configPolicy); this.entitySelector = entitySelector; this.moveSelectorList = moveSelectorList; phaseLifecycleSupport.addEventListener(entitySelector); @@ -33,7 +36,8 @@ public Iterator<Placement<Solution_>> iterator() { @Override public EntityPlacer<Solution_> rebuildWithFilter(SelectionFilter<Solution_, Object> filter) { - return new QueuedEntityPlacer<>(FilteringEntitySelector.of(entitySelector, filter), moveSelectorList); + return new QueuedEntityPlacer<>(factory, configPolicy, FilteringEntitySelector.of(entitySelector, filter), + moveSelectorList); } private class QueuedEntityPlacingIterator extends UpcomingSelectionIterator<Placement<Solution_>> { 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<Solution_> buildEntityPlacer(HeuristicConfigPolicy<Sol .buildMoveSelector(configPolicy, SelectionCacheType.JUST_IN_TIME, SelectionOrder.ORIGINAL, false); moveSelectorList.add(moveSelector); } - return new QueuedEntityPlacer<>(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<Solution_> extends AbstractEntityPlacer<Solution_ protected final EntityIndependentValueSelector<Solution_> valueSelector; protected final MoveSelector<Solution_> moveSelector; - public QueuedValuePlacer(EntityIndependentValueSelector<Solution_> valueSelector, - MoveSelector<Solution_> moveSelector) { + public QueuedValuePlacer(EntityPlacerFactory<Solution_> factory, HeuristicConfigPolicy<Solution_> configPolicy, + EntityIndependentValueSelector<Solution_> valueSelector, MoveSelector<Solution_> moveSelector) { + super(factory, configPolicy); this.valueSelector = valueSelector; this.moveSelector = moveSelector; phaseLifecycleSupport.addEventListener(valueSelector); @@ -59,7 +61,7 @@ protected Placement<Solution_> createUpcomingSelection() { @Override public EntityPlacer<Solution_> rebuildWithFilter(SelectionFilter<Solution_, Object> filter) { - return new QueuedValuePlacer<>( + return new QueuedValuePlacer<>(factory, configPolicy, (EntityIndependentFilteringValueSelector<Solution_>) 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<Solution_> buildEntityPlacer(HeuristicConfigPolicy<Solu + " Check your @" + ValueRangeProvider.class.getSimpleName() + " annotations."); } - return new QueuedValuePlacer<>((EntityIndependentValueSelector<Solution_>) valueSelector, moveSelector); + return new QueuedValuePlacer<>(this, configPolicy, (EntityIndependentValueSelector<Solution_>) valueSelector, + moveSelector); } private ValueSelectorConfig buildValueSelectorConfig(HeuristicConfigPolicy<Solution_> 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<Solution_> cloneBuilder() { .withLogIndentation(logIndentation); } + public HeuristicConfigPolicy<Solution_> copyConfigPolicy() { + return cloneBuilder() + .withEntitySorterManner(entitySorterManner) + .withValueSorterManner(valueSorterManner) + .withReinitializeVariableFilterEnabled(reinitializeVariableFilterEnabled) + .withInitializedChainedValueFilterEnabled(initializedChainedValueFilterEnabled) + .withUnassignedValuesAllowed(unassignedValuesAllowed) + .build(); + } + public HeuristicConfigPolicy<Solution_> 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<Solution_> +public final class RuinRecreateConstructionHeuristicPhase<Solution_> extends DefaultConstructionHeuristicPhase<Solution_> implements ConstructionHeuristicPhase<Solution_> { + private final Set<Object> elementsToRuinSet; + // Store the original value list of elements that are not included in the initial list of ruined elements + private final Map<Object, List<Object>> missingUpdatedElementsMap; + RuinRecreateConstructionHeuristicPhase(RuinRecreateConstructionHeuristicPhaseBuilder<Solution_> 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<Solution_> 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<Object, List<Object>> 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 <Solution_> RuinRecreateConstructionHeuristicPhaseBuilder<Solution HeuristicConfigPolicy<Solution_> solverConfigPolicy, ConstructionHeuristicPhaseConfig constructionHeuristicConfig) { var constructionHeuristicPhaseFactory = new RuinRecreateConstructionHeuristicPhaseFactory<Solution_>(constructionHeuristicConfig); - return (RuinRecreateConstructionHeuristicPhaseBuilder<Solution_>) constructionHeuristicPhaseFactory.getBuilder(0, false, + var builder = (RuinRecreateConstructionHeuristicPhaseBuilder<Solution_>) constructionHeuristicPhaseFactory.getBuilder(0, + false, solverConfigPolicy, TerminationFactory.<Solution_> create(new TerminationConfig()) .buildTermination(solverConfigPolicy)); + if (solverConfigPolicy.getMoveThreadCount() != null && solverConfigPolicy.getMoveThreadCount() >= 1) { + builder.multithreaded = true; + } + return builder; } - private List<Object> elementsToRecreate; + private final HeuristicConfigPolicy<Solution_> configPolicy; + private final RuinRecreateConstructionHeuristicPhaseFactory<Solution_> constructionHeuristicPhaseFactory; + private final Termination<Solution_> phaseTermination; + + Set<Object> elementsToRuin; + List<Object> elementsToRecreate; + private boolean multithreaded = false; - RuinRecreateConstructionHeuristicPhaseBuilder(Termination<Solution_> phaseTermination, - EntityPlacer<Solution_> entityPlacer, ConstructionHeuristicDecider<Solution_> decider) { + RuinRecreateConstructionHeuristicPhaseBuilder(HeuristicConfigPolicy<Solution_> configPolicy, + RuinRecreateConstructionHeuristicPhaseFactory<Solution_> constructionHeuristicPhaseFactory, + Termination<Solution_> phaseTermination, EntityPlacer<Solution_> entityPlacer, + ConstructionHeuristicDecider<Solution_> 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<Solution_> + ensureThreadSafe(InnerScoreDirector<Solution_, ?> scoreDirector) { + if (multithreaded && scoreDirector.isDerived()) { + return new RuinRecreateConstructionHeuristicPhaseBuilder<>(configPolicy, constructionHeuristicPhaseFactory, + phaseTermination, super.getEntityPlacer().copy(), + constructionHeuristicPhaseFactory.buildDecider(configPolicy, phaseTermination)); + } + return this; } public RuinRecreateConstructionHeuristicPhaseBuilder<Solution_> withElementsToRecreate(List<Object> elements) { @@ -41,12 +76,18 @@ public RuinRecreateConstructionHeuristicPhaseBuilder<Solution_> withElementsToRe return this; } + public RuinRecreateConstructionHeuristicPhaseBuilder<Solution_> withElementsToRuin(Set<Object> elements) { + this.elementsToRuin = elements; + return this; + } + @Override public EntityPlacer<Solution_> 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<Solution_> createBuilder( Termination<Solution_> solverTermination, int phaseIndex, boolean triggerFirstInitializedSolutionEvent, EntityPlacer<Solution_> entityPlacer) { var phaseTermination = new PhaseToSolverTerminationBridge<>(new BasicPlumbingTermination<Solution_>(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<Solution_> extends AbstractMove<Solution_> { @@ -38,21 +38,23 @@ public RuinRecreateMove(GenuineVariableDescriptor<Solution_> genuineVariableDesc protected void doMoveOnGenuineVariables(ScoreDirector<Solution_> scoreDirector) { recordedNewValues = new Object[ruinedEntityList.size()]; - var castScoreDirector = (VariableDescriptorAwareScoreDirector<Solution_>) scoreDirector; + var recordingScoreDirector = (VariableChangeRecordingScoreDirector<Solution_>) 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<Solution_> extends AbstractMove<Solution_> { - private final ListVariableStateSupply<Solution_> listVariableStateSupply; + private final ListVariableDescriptor<Solution_> listVariableDescriptor; private final List<Object> ruinedValueList; private final Set<Object> affectedEntitySet; private final RuinRecreateConstructionHeuristicPhaseBuilder<Solution_> constructionHeuristicPhaseBuilder; private final SolverScope<Solution_> solverScope; private final Map<Object, NavigableSet<RuinedLocation>> entityToNewPositionMap; - public ListRuinRecreateMove(ListVariableStateSupply<Solution_> listVariableStateSupply, + public ListRuinRecreateMove(ListVariableDescriptor<Solution_> listVariableDescriptor, RuinRecreateConstructionHeuristicPhaseBuilder<Solution_> constructionHeuristicPhaseBuilder, SolverScope<Solution_> solverScope, List<Object> ruinedValueList, Set<Object> affectedEntitySet) { - this.listVariableStateSupply = listVariableStateSupply; + this.listVariableDescriptor = listVariableDescriptor; this.constructionHeuristicPhaseBuilder = constructionHeuristicPhaseBuilder; this.solverScope = solverScope; this.ruinedValueList = ruinedValueList; @@ -41,63 +43,106 @@ public ListRuinRecreateMove(ListVariableStateSupply<Solution_> listVariableState @Override protected void doMoveOnGenuineVariables(ScoreDirector<Solution_> scoreDirector) { entityToNewPositionMap.clear(); - var entityToOriginalPositionMap = - CollectionUtils.<Object, NavigableSet<RuinedLocation>> 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<Solution_>) scoreDirector; + try (var listVariableStateSupply = variableChangeRecordingScoreDirector.getBacking().getSupplyManager() + .demand(listVariableDescriptor.getStateDemand())) { + var entityToOriginalPositionMap = + CollectionUtils.<Object, NavigableSet<RuinedLocation>> 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<Solution_>) 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<Solution_>) constructionHeuristicPhaseBuilder + .ensureThreadSafe(variableChangeRecordingScoreDirector.getBacking()) + .withElementsToRuin(entityToOriginalPositionMap.keySet()) + .withElementsToRecreate(ruinedValueList) + .build(); + + var nestedSolverScope = new SolverScope<Solution_>(); + 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.<Object, List<Object>> 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.<Object, List<Object>> 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<Solution_> scoreDirector) { public Move<Solution_> rebase(ScoreDirector<Solution_> destinationScoreDirector) { var rebasedRuinedValueList = AbstractMove.rebaseList(ruinedValueList, destinationScoreDirector); var rebasedAffectedEntitySet = AbstractMove.rebaseSet(affectedEntitySet, destinationScoreDirector); - return new ListRuinRecreateMove<>(listVariableStateSupply, constructionHeuristicPhaseBuilder, solverScope, + var rebasedListVariableDescriptor = ((InnerScoreDirector<Solution_, ?>) 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<Solution_> 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<Solution_> extends MoveDirector<Solutio super(new VariableChangeRecordingScoreDirector<>(scoreDirector, false)); } + @SuppressWarnings("unchecked") public Move<Solution_> createUndoMove() { - return new RecordedUndoMove<>(getVariableChangeRecordingScoreDirector().copyVariableChanges()); + var changes = (List<ChangeAction<Solution_>>) getVariableChangeRecordingScoreDirector().copyChanges(); + return new RecordedUndoMove<>(changes); } @Override public <Entity_, Value_> @NonNull ElementLocation getPositionOf( @NonNull PlanningListVariableMetaModel<Solution_, Entity_, Value_> 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<Solution_> solutionView) { .toList()); } + List<ChangeAction<Solution_>> 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<Solution_> implements VariableDescriptorAwareScoreDirector<Solution_> { +public final class VariableChangeRecordingScoreDirector<Solution_> + implements RevertableScoreDirector<Solution_> { - private final InnerScoreDirector<Solution_, ?> delegate; + private final InnerScoreDirector<Solution_, ?> backingScoreDirector; private final List<ChangeAction<Solution_>> 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<Solution_> implements Va */ private final Map<Object, Integer> cache; - public VariableChangeRecordingScoreDirector(ScoreDirector<Solution_> delegate) { - this(delegate, true); + public VariableChangeRecordingScoreDirector(ScoreDirector<Solution_> backingScoreDirector) { + this(backingScoreDirector, true); } - public VariableChangeRecordingScoreDirector(ScoreDirector<Solution_> delegate, boolean requiresIndexCache) { - this.delegate = (InnerScoreDirector<Solution_, ?>) delegate; + public VariableChangeRecordingScoreDirector(ScoreDirector<Solution_> backingScoreDirector, boolean requiresIndexCache) { + this.backingScoreDirector = (InnerScoreDirector<Solution_, ?>) 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<ChangeAction<Solution_>> copyVariableChanges() { + private VariableChangeRecordingScoreDirector(InnerScoreDirector<Solution_, ?> backingScoreDirector, + List<ChangeAction<Solution_>> variableChanges, Map<Object, Integer> 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<Solution_> 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<Solution_> variableDescriptor, Object entity) { - delegate.afterVariableChanged(variableDescriptor, entity); + if (backingScoreDirector != null) { + backingScoreDirector.afterVariableChanged(variableDescriptor, entity); + } } @Override @@ -92,7 +109,9 @@ public void beforeListVariableChanged(ListVariableDescriptor<Solution_> 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<Solution_> 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<Solution_> 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<Solution_> 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<Solution_> 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<Solution_> getSolutionDescriptor() { - return delegate.getSolutionDescriptor(); + return Objects.requireNonNull(backingScoreDirector).getSolutionDescriptor(); + } + + /** + * Returns the score director to which events are delegated. + */ + public InnerScoreDirector<Solution_, ?> getBacking() { + return backingScoreDirector; } - public InnerScoreDirector<Solution_, ?> 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<Solution_> getNonDelegating() { + return new VariableChangeRecordingScoreDirector<>(null, variableChanges, cache); } @Override public Solution_ getWorkingSolution() { - return delegate.getWorkingSolution(); + return Objects.requireNonNull(backingScoreDirector).getWorkingSolution(); } @Override public VariableDescriptorCache<Solution_> getVariableDescriptorCache() { - return delegate.getVariableDescriptorCache(); + return Objects.requireNonNull(backingScoreDirector).getVariableDescriptorCache(); } @Override public void triggerVariableListeners() { - delegate.triggerVariableListeners(); + if (backingScoreDirector != null) { + backingScoreDirector.triggerVariableListeners(); + } } @Override public <E> E lookUpWorkingObject(E externalObject) { - return delegate.lookUpWorkingObject(externalObject); + return Objects.requireNonNull(backingScoreDirector).lookUpWorkingObject(externalObject); } @Override public <E> E lookUpWorkingObjectOrReturnNull(E externalObject) { - return delegate.lookUpWorkingObjectOrReturnNull(externalObject); + return Objects.requireNonNull(backingScoreDirector).lookUpWorkingObjectOrReturnNull(externalObject); } @Override @@ -178,29 +222,4 @@ public void changeVariableFacade(VariableDescriptor<Solution_> 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<Solution_> variableDescriptor, Object entity, - List<Object> 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<Score_> 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<Solution_> extends VariableDescriptorAwareScoreDirector<Solution_> { + + /** + * Use this method to get a copy of all non-commited changes executed by the director so far. + * + * @param <Action_> The action type for recorded changes + */ + <Action_> List<Action_> 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<Score_> 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<TestdataSolution> moveSelector = SelectorTestUtils.mockMoveSelector( + var moveSelector = SelectorTestUtils.mockMoveSelector( new DummyMove("a1"), new DummyMove("a2"), new DummyMove("b1")); - PooledEntityPlacer<TestdataSolution> placer = new PooledEntityPlacer<>(moveSelector); + var placer = new PooledEntityPlacer<>(null, null, moveSelector); - SolverScope<TestdataSolution> solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope<TestdataSolution> phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator<Placement<TestdataSolution>> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataSolution> 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<TestdataSolution> 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<TestdataSolution> 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<TestdataSolution> phaseScopeB = mock(AbstractPhaseScope.class); + var phaseScopeB = mock(AbstractPhaseScope.class); when(phaseScopeB.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeB); placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataSolution> 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<TestdataSolution>(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<TestdataSolution> valueSelector = SelectorTestUtils.mockValueSelector(TestdataEntity.class, "value", new TestdataValue("1"), new TestdataValue("2")); - MoveSelector<TestdataSolution> moveSelector = + var moveSelector = new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), valueSelector, false); - QueuedEntityPlacer<TestdataSolution> placer = - new QueuedEntityPlacer<>(recordingEntitySelector, Collections.singletonList(moveSelector)); + var placer = new QueuedEntityPlacer<>(null, null, recordingEntitySelector, Collections.singletonList(moveSelector)); - SolverScope<TestdataSolution> solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope<TestdataSolution> phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); - Iterator<Placement<TestdataSolution>> placementIterator = placer.iterator(); + var placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataSolution> 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<TestdataSolution> 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<TestdataSolution> 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<TestdataSolution> phaseScopeB = mock(AbstractPhaseScope.class); + var phaseScopeB = mock(AbstractPhaseScope.class); when(phaseScopeB.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeB); placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataSolution> 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<MoveSelector<TestdataMultiVarSolution>> moveSelectorList = new ArrayList<>(2); + var moveSelectorList = new ArrayList<MoveSelector<TestdataMultiVarSolution>>(2); moveSelectorList.add(new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), primaryValueSelector, false)); @@ -123,40 +127,40 @@ void multiQueuedMoveSelector() { new MimicReplayingEntitySelector<>(recordingEntitySelector), secondaryValueSelector, false)); - QueuedEntityPlacer<TestdataMultiVarSolution> placer = - new QueuedEntityPlacer<>(recordingEntitySelector, moveSelectorList); + var placer = + new QueuedEntityPlacer<>(null, null, recordingEntitySelector, moveSelectorList); - SolverScope<TestdataMultiVarSolution> solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope<TestdataMultiVarSolution> phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator<Placement<TestdataMultiVarSolution>> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataMultiVarSolution> 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<TestdataMultiVarSolution> 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<TestdataMultiVarSolution> 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<TestdataMultiVarSolution> 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<MoveSelector<TestdataMultiVarSolution>> moveSelectorList = new ArrayList<>(2); + var moveSelectorList = new ArrayList<MoveSelector<TestdataMultiVarSolution>>(2); moveSelectorList.add(new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), primaryValueSelector, false)); moveSelectorList.add(new ChangeMoveSelector<>(new MimicReplayingEntitySelector<>(recordingEntitySelector), secondaryValueSelector, false)); - MoveSelector<TestdataMultiVarSolution> moveSelector = new CartesianProductMoveSelector<>(moveSelectorList, true, false); - QueuedEntityPlacer<TestdataMultiVarSolution> placer = new QueuedEntityPlacer<>(recordingEntitySelector, + var moveSelector = new CartesianProductMoveSelector<>(moveSelectorList, true, false); + var placer = new QueuedEntityPlacer<>(null, null, recordingEntitySelector, Collections.singletonList(moveSelector)); - SolverScope<TestdataMultiVarSolution> solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope<TestdataMultiVarSolution> phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator<Placement<TestdataMultiVarSolution>> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataMultiVarSolution> 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<TestdataMultiVarSolution> 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<TestdataMultiVarSolution> entitySelector = + SelectorTestUtils.mockEntitySelector(TestdataMultiVarEntity.class, + new TestdataMultiVarEntity("a"), new TestdataMultiVarEntity("b")); + MimicRecordingEntitySelector<TestdataMultiVarSolution> recordingEntitySelector = + new MimicRecordingEntitySelector<>(entitySelector); + ValueSelector<TestdataMultiVarSolution> primaryValueSelector = SelectorTestUtils.mockValueSelector( + TestdataMultiVarEntity.class, "primaryValue", + new TestdataValue("1"), new TestdataValue("2"), new TestdataValue("3")); + ValueSelector<TestdataMultiVarSolution> secondaryValueSelector = SelectorTestUtils.mockValueSelector( + TestdataMultiVarEntity.class, "secondaryValue", + new TestdataValue("8"), new TestdataValue("9")); + var moveSelectorList = new ArrayList<MoveSelector<TestdataMultiVarSolution>>(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<TestdataMultiVarSolution>(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<TestdataSolution> variableDescriptor = TestdataEntity.buildVariableDescriptorForValue(); + var variableDescriptor = TestdataEntity.buildVariableDescriptorForValue(); EntitySelector<TestdataSolution> entitySelector = SelectorTestUtils.mockEntitySelector(variableDescriptor.getEntityDescriptor(), new TestdataEntity("a"), new TestdataEntity("b"), new TestdataEntity("c")); @@ -41,34 +45,34 @@ void oneMoveSelector() { MimicRecordingValueSelector<TestdataSolution> recordingValueSelector = new MimicRecordingValueSelector<>(valueSelector); - MoveSelector<TestdataSolution> moveSelector = new ChangeMoveSelector<>(entitySelector, + var moveSelector = new ChangeMoveSelector<>(entitySelector, new MimicReplayingValueSelector<>(recordingValueSelector), false); - QueuedValuePlacer<TestdataSolution> placer = new QueuedValuePlacer<>(recordingValueSelector, moveSelector); + var placer = new QueuedValuePlacer<>(null, null, recordingValueSelector, moveSelector); - SolverScope<TestdataSolution> solverScope = mock(SolverScope.class); + var solverScope = mock(SolverScope.class); placer.solvingStarted(solverScope); - AbstractPhaseScope<TestdataSolution> phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeA); Iterator<Placement<TestdataSolution>> placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataSolution> 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<TestdataSolution> 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<TestdataSolution> 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<TestdataSolution> phaseScopeB = mock(AbstractPhaseScope.class); + var phaseScopeB = mock(AbstractPhaseScope.class); when(phaseScopeB.getSolverScope()).thenReturn(solverScope); placer.phaseStarted(phaseScopeB); placementIterator = placer.iterator(); assertThat(placementIterator).hasNext(); - AbstractStepScope<TestdataSolution> 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<TestdataSolution> entitySelector = + SelectorTestUtils.mockEntitySelector(variableDescriptor.getEntityDescriptor(), + new TestdataEntity("a"), new TestdataEntity("b"), new TestdataEntity("c")); + EntityIndependentValueSelector<TestdataSolution> valueSelector = SelectorTestUtils.mockEntityIndependentValueSelector( + variableDescriptor, + new TestdataValue("1"), new TestdataValue("2")); + MimicRecordingValueSelector<TestdataSolution> 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<TestdataSolution>(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<TestdataSolution>() + .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<TestdataSolution>() + .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<TestdataListSolution>(mock(ListVariableStateSupply.class), + var move = new ListRuinRecreateMove<TestdataListSolution>(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<TestdataListSolution>(supply, + var descriptor = mock(ListVariableDescriptor.class); + var move = new ListRuinRecreateMove<TestdataListSolution>(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), Set.of(v1)); - var sameMove = new ListRuinRecreateMove<TestdataListSolution>(supply, + var sameMove = new ListRuinRecreateMove<TestdataListSolution>(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), Set.of(v1)); assertThat(move).isEqualTo(sameMove); - var differentMove = new ListRuinRecreateMove<TestdataListSolution>(supply, + var differentMove = new ListRuinRecreateMove<TestdataListSolution>(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), Set.of(v2)); assertThat(move).isNotEqualTo(differentMove); - var anotherDifferentMove = new ListRuinRecreateMove<TestdataListSolution>(supply, + var anotherDifferentMove = new ListRuinRecreateMove<TestdataListSolution>(descriptor, mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e2), Set.of(v1)); assertThat(move).isNotEqualTo(anotherDifferentMove); - var yetAnotherDifferentMove = new ListRuinRecreateMove<TestdataListSolution>(mock(ListVariableStateSupply.class), + var yetAnotherDifferentMove = new ListRuinRecreateMove<TestdataListSolution>(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<TestdataListSolution>(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<TestdataListSolution>(listVariableDescriptor, + ruinRecreateConstructionHeuristicPhaseBuilder, mock(SolverScope.class), Arrays.asList(v1), Set.of(e1)); + move.doMoveOnly(scoreDirector); + var undoMove = (RecordedUndoMove<TestdataListSolution>) 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(); + } + } + }