Skip to content

Commit

Permalink
fix: fail fast when local search starts from an uninitialized solution
Browse files Browse the repository at this point in the history
  • Loading branch information
triceo committed Jun 20, 2024
1 parent 80524a4 commit 5690ec6
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -800,15 +800,6 @@ public boolean isMovable(ScoreDirector<Solution_> scoreDirector, Object entity)
|| effectiveMovableEntitySelectionFilter.accept(scoreDirector, entity));
}

/**
* @param scoreDirector never null
* @param entity never null
* @return true if the entity is initialized or pinned
*/
public boolean isEntityInitializedOrPinned(ScoreDirector<Solution_> scoreDirector, Object entity) {
return !isGenuine() || isInitialized(entity) || !isMovable(scoreDirector, entity);
}

@Override
public String toString() {
return getClass().getSimpleName() + "(" + entityClass.getName() + ")";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.config.solver.monitoring.SolverMetric;
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
import ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhase;
import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener;
import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport;
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope;
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
import ai.timefold.solver.core.impl.solver.AbstractSolver;
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
import ai.timefold.solver.core.impl.solver.termination.Termination;
Expand Down Expand Up @@ -184,28 +180,29 @@ public void removePhaseLifecycleListener(PhaseLifecycleListener<Solution_> phase

protected void assertWorkingSolutionInitialized(AbstractPhaseScope<Solution_> phaseScope) {
if (!phaseScope.getStartingScore().isSolutionInitialized()) {
InnerScoreDirector<Solution_, ?> scoreDirector = phaseScope.getScoreDirector();
SolutionDescriptor<Solution_> solutionDescriptor = scoreDirector.getSolutionDescriptor();
Solution_ workingSolution = scoreDirector.getWorkingSolution();
solutionDescriptor.visitAllEntities(workingSolution, entity -> {
EntityDescriptor<Solution_> entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(
entity.getClass());
if (!entityDescriptor.isEntityInitializedOrPinned(scoreDirector, entity)) {
String variableRef = null;
for (GenuineVariableDescriptor<Solution_> variableDescriptor : entityDescriptor
.getGenuineVariableDescriptorList()) {
if (!variableDescriptor.isInitialized(entity)) {
variableRef = variableDescriptor.getSimpleEntityAndVariableName();
break;
}
}
throw new IllegalStateException(getPhaseTypeString() + " phase (" + phaseIndex
+ ") needs to start from an initialized solution, but the planning variable (" + variableRef
+ ") is uninitialized for the entity (" + entity + ").\n"
+ "Maybe there is no Construction Heuristic configured before this phase to initialize the solution.\n"
+ "Or maybe the getter/setters of your planning variables in your domain classes aren't implemented correctly.");
}
});
var scoreDirector = phaseScope.getScoreDirector();
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var workingSolution = scoreDirector.getWorkingSolution();
var initializationStatistics = solutionDescriptor.computeInitializationStatistics(workingSolution);
var uninitializedEntityCount = initializationStatistics.uninitializedEntityCount();
if (uninitializedEntityCount > 0) {
throw new IllegalStateException(
"""
%s phase (%d) needs to start from an initialized solution, but there are (%d) uninitialized entities.
Maybe there is no Construction Heuristic configured before this phase to initialize the solution.
Or maybe the getter/setters of your planning variables in your domain classes aren't implemented correctly."""
.formatted(getPhaseTypeString(), phaseIndex, uninitializedEntityCount));
}
var unassignedValueCount = initializationStatistics.unassignedValueCount();
if (unassignedValueCount > 0) {
throw new IllegalStateException(
"""
%s phase (%d) needs to start from an initialized solution, \
but planning list variable (%s) has (%d) unexpected unassigned values.
Maybe there is no Construction Heuristic configured before this phase to initialize the solution."""
.formatted(getPhaseTypeString(), phaseIndex, solutionDescriptor.getListVariableDescriptor(),
unassignedValueCount));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,28 @@ void solveListVariableWithExternalizedInverseAndIndexSupplies() {
assertThat(solution).isNotNull();
}

@Test
void failsFastWithUninitializedSolutionBasicVariable() {
var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class);
solverConfig.withPhases(solverConfig.getPhaseConfigList().get(1)); // Remove construction heuristic.

var solution = PlannerTestUtils.generateTestdataSolution("s1", 2);

assertThatThrownBy(() -> PlannerTestUtils.solve(solverConfig, solution))
.hasMessageContaining("uninitialized entities");
}

@Test
void failsFastWithUninitializedSolutionListVariable() {
var solverConfig = PlannerTestUtils.buildSolverConfig(
TestdataListSolutionExternalized.class, TestdataListEntityExternalized.class);
solverConfig.withPhases(solverConfig.getPhaseConfigList().get(1)); // Remove construction heuristic.

var solution = TestdataListSolutionExternalized.generateUninitializedSolution(6, 2);

assertThatThrownBy(() -> PlannerTestUtils.solve(solverConfig, solution))
.hasMessageContaining("planning list variable")
.hasMessageContaining("unexpected unassigned values");
}

}

0 comments on commit 5690ec6

Please sign in to comment.