From 9c36871cb100a503b47b1abdbdc0fde2dec69d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 19 Sep 2024 08:08:55 +0200 Subject: [PATCH] feat: multi-threaded solving support for Ruin and Recreate moves --- .../impl/heuristic/move/AbstractMove.java | 16 +-- .../move/generic/RuinRecreateMove.java | 25 +++++ .../list/ruin/ListRuinRecreateMove.java | 25 +++++ .../move/generic/RuinRecreateMoveTest.java | 95 ++++++++++++++++++ .../list/ListRuinRecreateMoveTest.java | 98 +++++++++++++++++++ 5 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveTest.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java index 99dcbbb35d..7f00c75282 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java @@ -1,7 +1,9 @@ package ai.timefold.solver.core.impl.heuristic.move; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.director.ScoreDirector; @@ -50,19 +52,19 @@ public final void doMoveOnly(ScoreDirector scoreDirector) { // ************************************************************************ public static List rebaseList(List externalObjectList, ScoreDirector destinationScoreDirector) { - List rebasedObjectList = new ArrayList<>(externalObjectList.size()); - for (E entity : externalObjectList) { + var rebasedObjectList = new ArrayList(externalObjectList.size()); + for (var entity : externalObjectList) { rebasedObjectList.add(destinationScoreDirector.lookUpWorkingObject(entity)); } return rebasedObjectList; } - public static Object[] rebaseArray(Object[] externalObjects, ScoreDirector destinationScoreDirector) { - Object[] rebasedObjects = new Object[externalObjects.length]; - for (int i = 0; i < externalObjects.length; i++) { - rebasedObjects[i] = destinationScoreDirector.lookUpWorkingObject(externalObjects[i]); + public static Set rebaseSet(Set externalObjectSet, ScoreDirector destinationScoreDirector) { + var rebasedObjectSet = new LinkedHashSet(externalObjectSet.size()); + for (var entity : externalObjectSet) { + rebasedObjectSet.add(destinationScoreDirector.lookUpWorkingObject(entity)); } - return rebasedObjects; + return rebasedObjectSet; } } 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 a4e064ad1e..aa85b41297 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 @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Set; import ai.timefold.solver.core.api.score.director.ScoreDirector; @@ -82,6 +83,30 @@ public boolean isMoveDoable(ScoreDirector scoreDirector) { return true; } + @Override + public Move rebase(ScoreDirector destinationScoreDirector) { + var rebasedRuinedEntityList = rebaseList(ruinedEntityList, destinationScoreDirector); + var rebasedAffectedValueSet = rebaseSet(affectedValueSet, destinationScoreDirector); + return new RuinRecreateMove<>(genuineVariableDescriptor, constructionHeuristicPhaseBuilder, solverScope, + rebasedRuinedEntityList, rebasedAffectedValueSet); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof RuinRecreateMove that)) + return false; + return Objects.equals(genuineVariableDescriptor, that.genuineVariableDescriptor) + && Objects.equals(ruinedEntityList, that.ruinedEntityList) + && Objects.equals(affectedValueSet, that.affectedValueSet); + } + + @Override + public int hashCode() { + return Objects.hash(genuineVariableDescriptor, ruinedEntityList, affectedValueSet); + } + @Override public String toString() { return "RuinMove{" + 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 136306cbef..1232f36455 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 @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; import java.util.NavigableSet; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; @@ -105,6 +106,30 @@ public boolean isMoveDoable(ScoreDirector scoreDirector) { return true; } + @Override + public Move rebase(ScoreDirector destinationScoreDirector) { + var rebasedRuinedValueList = rebaseList(ruinedValueList, destinationScoreDirector); + var rebasedAffectedEntitySet = rebaseSet(affectedEntitySet, destinationScoreDirector); + return new ListRuinRecreateMove<>(listVariableStateSupply, constructionHeuristicPhaseBuilder, solverScope, + rebasedRuinedValueList, rebasedAffectedEntitySet); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof ListRuinRecreateMove that)) + return false; + return Objects.equals(listVariableStateSupply, that.listVariableStateSupply) + && Objects.equals(ruinedValueList, that.ruinedValueList) + && Objects.equals(affectedEntitySet, that.affectedEntitySet); + } + + @Override + public int hashCode() { + return Objects.hash(listVariableStateSupply, ruinedValueList, affectedEntitySet); + } + @Override public String toString() { return "ListRuinMove{" + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveTest.java new file mode 100644 index 0000000000..17b9c211a2 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveTest.java @@ -0,0 +1,95 @@ +package ai.timefold.solver.core.impl.heuristic.selector.move.generic; + +import static ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils.mockRebasingScoreDirector; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +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; + +import org.junit.jupiter.api.Test; + +class RuinRecreateMoveTest { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + void rebase() { + var variableDescriptor = TestdataEntity.buildVariableDescriptorForValue(); + + var v1 = new TestdataValue("v1"); + var v2 = new TestdataValue("v2"); + var e1 = new TestdataEntity("e1", v1); + var e2 = new TestdataEntity("e2", null); + var e3 = new TestdataEntity("e3", v1); + + var destinationV1 = new TestdataValue("v1"); + var destinationV2 = new TestdataValue("v2"); + var destinationE1 = new TestdataEntity("e1", destinationV1); + var destinationE2 = new TestdataEntity("e2", null); + var destinationE3 = new TestdataEntity("e3", destinationV1); + + var destinationScoreDirector = mockRebasingScoreDirector( + variableDescriptor.getEntityDescriptor().getSolutionDescriptor(), new Object[][] { + { v1, destinationV1 }, + { v2, destinationV2 }, + { e1, destinationE1 }, + { e2, destinationE2 }, + { e3, destinationE3 }, + }); + + var move = new RuinRecreateMove(mock(GenuineVariableDescriptor.class), + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), Arrays.asList(e1, e2, e3), + Set.of(v1, v2)); + var rebasedMove = move.rebase(destinationScoreDirector); + + assertSoftly(softly -> { + softly.assertThat((Collection) rebasedMove.getPlanningEntities()) + .containsExactly(destinationE1, destinationE2, destinationE3); + softly.assertThat((Collection) rebasedMove.getPlanningValues()) + .containsExactlyInAnyOrder(destinationV1, destinationV2); // The input set is not ordered. + }); + + } + + @SuppressWarnings("unchecked") + @Test + void equality() { + var v1 = new TestdataValue("v1"); + var v2 = new TestdataValue("v2"); + var e1 = new TestdataEntity("e1", v1); + var e2 = new TestdataEntity("e2", null); + + var descriptor = mock(GenuineVariableDescriptor.class); + var move = new RuinRecreateMove(descriptor, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), + Set.of(v1)); + var sameMove = new RuinRecreateMove(descriptor, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), + Set.of(v1)); + assertThat(move).isEqualTo(sameMove); + + var differentMove = new RuinRecreateMove(descriptor, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), + Set.of(v2)); + assertThat(move).isNotEqualTo(differentMove); + + var anotherDifferentMove = new RuinRecreateMove(descriptor, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e2), + Set.of(v1)); + assertThat(move).isNotEqualTo(anotherDifferentMove); + + var yetAnotherDifferentMove = new RuinRecreateMove(mock(GenuineVariableDescriptor.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/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java new file mode 100644 index 0000000000..132600ecec --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveTest.java @@ -0,0 +1,98 @@ +package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list; + +import static ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils.mockRebasingScoreDirector; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +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; +import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListEntity; +import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListSolution; +import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListValue; + +import org.junit.jupiter.api.Test; + +class ListRuinRecreateMoveTest { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + void rebase() { + var variableDescriptor = TestdataListEntity.buildVariableDescriptorForValueList(); + + var v1 = new TestdataListValue("v1"); + var v2 = new TestdataListValue("v2"); + var e1 = new TestdataListEntity("e1", v1); + var e2 = new TestdataListEntity("e2"); + var e3 = new TestdataListEntity("e3", v1); + + var destinationV1 = new TestdataListValue("v1"); + var destinationV2 = new TestdataListValue("v2"); + var destinationE1 = new TestdataListEntity("e1", destinationV1); + var destinationE2 = new TestdataListEntity("e2"); + var destinationE3 = new TestdataListEntity("e3", destinationV1); + + var destinationScoreDirector = mockRebasingScoreDirector( + variableDescriptor.getEntityDescriptor().getSolutionDescriptor(), new Object[][] { + { v1, destinationV1 }, + { v2, destinationV2 }, + { e1, destinationE1 }, + { e2, destinationE2 }, + { e3, destinationE3 }, + }); + + var move = new ListRuinRecreateMove(mock(ListVariableStateSupply.class), + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), Arrays.asList(v1, v2), + Set.of(e1, e2, e3)); + var rebasedMove = move.rebase(destinationScoreDirector); + + assertSoftly(softly -> { + softly.assertThat((Collection) rebasedMove.getPlanningEntities()) + .containsExactlyInAnyOrder(destinationE1, destinationE2, destinationE3); // The input set is not ordered. + softly.assertThat((Collection) rebasedMove.getPlanningValues()) + .containsExactly(destinationV1, destinationV2); + }); + + } + + @SuppressWarnings("unchecked") + @Test + void equality() { + var v1 = new TestdataListValue("v1"); + var v2 = new TestdataListValue("v2"); + var e1 = new TestdataListEntity("e1", v1); + var e2 = new TestdataListEntity("e2"); + + var supply = mock(ListVariableStateSupply.class); + var move = new ListRuinRecreateMove(supply, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), + Set.of(v1)); + var sameMove = new ListRuinRecreateMove(supply, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), + Set.of(v1)); + assertThat(move).isEqualTo(sameMove); + + var differentMove = new ListRuinRecreateMove(supply, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), + Set.of(v2)); + assertThat(move).isNotEqualTo(differentMove); + + var anotherDifferentMove = new ListRuinRecreateMove(supply, + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e2), + Set.of(v1)); + assertThat(move).isNotEqualTo(anotherDifferentMove); + + var yetAnotherDifferentMove = new ListRuinRecreateMove(mock(ListVariableStateSupply.class), + mock(RuinRecreateConstructionHeuristicPhaseBuilder.class), mock(SolverScope.class), List.of(e1), + Set.of(v1)); + assertThat(move).isNotEqualTo(yetAnotherDifferentMove); + } + +}