Skip to content

Commit

Permalink
feat: log a warning when CH terminated prematurely (#1362)
Browse files Browse the repository at this point in the history
Co-authored-by: Frederico Gonçalves <zepfred@users.noreply.github.com>
  • Loading branch information
triceo and zepfred authored Jan 29, 2025
1 parent e4805a8 commit 6537668
Show file tree
Hide file tree
Showing 32 changed files with 1,137 additions and 906 deletions.
29 changes: 22 additions & 7 deletions core/src/main/java/ai/timefold/solver/core/api/score/Score.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.io.Serializable;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
import ai.timefold.solver.core.api.score.buildin.simplebigdecimal.SimpleBigDecimalScore;
Expand Down Expand Up @@ -30,15 +32,18 @@ public interface Score<Score_ extends Score<Score_>>
extends Comparable<Score_>, Serializable {

/**
* The init score is the negative of the number of uninitialized genuine planning variables.
* If it's 0 (which it usually is), the {@link PlanningSolution} is fully initialized
* and the score's {@link Object#toString()} does not mention it.
* The init score is the negative of the number of genuine planning variables set to null,
* unless null values are specifically allowed by {@link PlanningVariable#allowsUnassigned()}
* or {@link PlanningListVariable#allowsUnassignedValues()}
* Nulls are typically only allowed in over-constrained planning.
* In that case, there is no way how to tell a fully initialized solution with some values left unassigned,
* from a partially initialized solution where the initialization of some values wasn't yet attempted.
* <p>
* During {@link #compareTo(Object)}, it's even more important than the hard score:
* if you don't want this behaviour, read about overconstrained planning in the reference manual.
* During {@link #compareTo(Object)}, init score is considered more important than the hard score.
* If the init score is 0 (which it usually is), the score's {@link Object#toString()} does not mention it.
*
* @return higher is better, always negative (except in statistical calculations), 0 if all planning variables are
* initialized
* @return higher is better, always negative (except in statistical calculations); 0 if all planning variables are
* non-null, or if nulls are allowed.
*/
default int initScore() {
// TODO remove default implementation in 2.0; exists only for backwards compatibility
Expand Down Expand Up @@ -185,6 +190,16 @@ default boolean isZero() {

/**
* Checks if the {@link PlanningSolution} of this score was fully initialized when it was calculated.
* This only works for solutions where:
* <ul>
* <li>{@link PlanningVariable basic variables} are used,
* and {@link PlanningVariable#allowsUnassigned() unassigning} is not allowed.</li>
* <li>{@link PlanningListVariable list variables} are used,
* and {@link PlanningListVariable#allowsUnassignedValues() unassigned values} are not allowed.</li>
* </ul>
*
* For solutions which do allow unassigning values,
* {@link #initScore()} is always zero and therefore this method always returns true.
*
* @return true if {@link #initScore()} is 0
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ public interface SolverJob<Solution_, ProblemId_> {
@NonNull
SolverStatus getSolverStatus();

// TODO Future features
// void reloadProblem(Function<? super ProblemId_, Solution_> problemFinder);

/**
* Schedules a {@link ProblemChange} to be processed by the underlying {@link Solver} and returns immediately.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import java.util.function.Function;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.score.Score;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;

/**
* Provides a fluent contract that allows customization and submission of planning problems to solve.
Expand Down Expand Up @@ -77,16 +79,30 @@ public interface SolverJobBuilder<Solution_, ProblemId_> {
withFinalBestSolutionConsumer(@NonNull Consumer<? super Solution_> finalBestSolutionConsumer);

/**
* Sets the consumer of the first initialized solution. First initialized solution is the solution at the end of
* the last phase that immediately precedes the first local search phase. This solution marks the beginning of actual
* optimization process.
* As defined by #withFirstInitializedSolutionConsumer(FirstInitializedSolutionConsumer).
*
* @deprecated Use {@link #withFirstInitializedSolutionConsumer(FirstInitializedSolutionConsumer)} instead.
*/
@Deprecated(forRemoval = true, since = "1.19.0")
@NonNull
default SolverJobBuilder<Solution_, ProblemId_>
withFirstInitializedSolutionConsumer(@NonNull Consumer<? super Solution_> firstInitializedSolutionConsumer) {
return withFirstInitializedSolutionConsumer(
(solution, isTerminatedEarly) -> firstInitializedSolutionConsumer.accept(solution));
}

/**
* Sets the consumer of the first initialized solution,
* the beginning of the actual optimization process.
* First initialized solution is the solution at the end of the last phase
* that immediately precedes the first local search phase.
*
* @param firstInitializedSolutionConsumer called only once before starting the first Local Search phase
* @return this
*/
@NonNull
SolverJobBuilder<Solution_, ProblemId_>
withFirstInitializedSolutionConsumer(@NonNull Consumer<? super Solution_> firstInitializedSolutionConsumer);
SolverJobBuilder<Solution_, ProblemId_> withFirstInitializedSolutionConsumer(
@NonNull FirstInitializedSolutionConsumer<? super Solution_> firstInitializedSolutionConsumer);

/**
* Sets the consumer for when the solver starts its solving process.
Expand Down Expand Up @@ -122,4 +138,31 @@ public interface SolverJobBuilder<Solution_, ProblemId_> {
*/
@NonNull
SolverJob<Solution_, ProblemId_> run();

/**
* A consumer that accepts the first initialized solution.
*
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
@NullMarked
interface FirstInitializedSolutionConsumer<Solution_> {

/**
* Accepts the first solution after initialization.
*
* @param solution the first solution after initialization phase(s) finished
* @param isTerminatedEarly false in most common cases.
* True if the solver was terminated early, before the solution could be fully initialized,
* typically as a result of construction heuristic running for too long
* and tripping a time-based termination condition.
* In that case, there will likely be no other phase after this one
* and the solver will terminate as well, without launching any optimizing phase.
* Therefore, the solution captured with {@link SolverJobBuilder#withBestSolutionConsumer(Consumer)}
* will likely be unchanged from this one.
* @see Score#initScore() Score Javadoc explains partial initialization and its consequences.
*/
void accept(Solution_ solution, boolean isTerminatedEarly);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.impl.phase.AbstractPhase;
import ai.timefold.solver.core.impl.phase.Phase;
import ai.timefold.solver.core.impl.phase.PossiblyInitializingPhase;

/**
* A {@link ConstructionHeuristicPhase} is a {@link Phase} which uses a construction heuristic algorithm,
Expand All @@ -13,6 +14,7 @@
* @see AbstractPhase
* @see DefaultConstructionHeuristicPhase
*/
public interface ConstructionHeuristicPhase<Solution_> extends Phase<Solution_> {
public interface ConstructionHeuristicPhase<Solution_>
extends PossiblyInitializingPhase<Solution_> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,42 @@
import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacer;
import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope;
import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope;
import ai.timefold.solver.core.impl.phase.AbstractPhase;
import ai.timefold.solver.core.impl.phase.AbstractPossiblyInitializingPhase;
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
import ai.timefold.solver.core.impl.solver.termination.Termination;

import org.jspecify.annotations.NullMarked;
import org.slf4j.event.Level;

/**
* Default implementation of {@link ConstructionHeuristicPhase}.
*
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
public class DefaultConstructionHeuristicPhase<Solution_> extends AbstractPhase<Solution_>
@NullMarked
public class DefaultConstructionHeuristicPhase<Solution_>
extends AbstractPossiblyInitializingPhase<Solution_>
implements ConstructionHeuristicPhase<Solution_> {

protected final ConstructionHeuristicDecider<Solution_> decider;
protected final EntityPlacer<Solution_> entityPlacer;
private TerminationStatus terminationStatus = TerminationStatus.NOT_TERMINATED;

protected DefaultConstructionHeuristicPhase(DefaultConstructionHeuristicPhaseBuilder<Solution_> builder) {
super(builder);
decider = builder.decider;
entityPlacer = builder.getEntityPlacer();
this.decider = builder.decider;
this.entityPlacer = builder.getEntityPlacer();
}

public EntityPlacer<Solution_> getEntityPlacer() {
return entityPlacer;
}

@Override
public TerminationStatus getTerminationStatus() {
return terminationStatus;
}

@Override
public String getPhaseTypeString() {
return "Construction Heuristics";
Expand All @@ -57,7 +66,10 @@ public void solve(SolverScope<Solution_> solverScope) {
maxStepCount = listVariableDescriptor.countUnassigned(workingSolution);
}

for (var placement : entityPlacer) {
var iterator = entityPlacer.iterator();
TerminationStatus earlyTerminationStatus = null;
while (iterator.hasNext()) {
var placement = iterator.next();
var stepScope = new ConstructionHeuristicStepScope<>(phaseScope);
stepStarted(stepScope);
decider.decideNextStep(stepScope, placement);
Expand All @@ -83,17 +95,23 @@ public void solve(SolverScope<Solution_> solverScope) {
+ ") has selected move count (" + stepScope.getSelectedMoveCount()
+ ") but failed to pick a nextStep (" + stepScope.getStep() + ").");
}
// Although stepStarted has been called, stepEnded is not called for this step
// Although stepStarted has been called, stepEnded is not called for this step.
earlyTerminationStatus = TerminationStatus.early(phaseScope.getNextStepIndex());
break;
}
doStep(stepScope);
stepEnded(stepScope);
phaseScope.setLastCompletedStepScope(stepScope);
if (phaseTermination.isPhaseTerminated(phaseScope)
|| (hasListVariable && stepScope.getStepIndex() >= maxStepCount)) {
if (hasListVariable && stepScope.getStepIndex() >= maxStepCount) {
earlyTerminationStatus = TerminationStatus.regular(phaseScope.getNextStepIndex());
break;
} else if (phaseTermination.isPhaseTerminated(phaseScope)) {
earlyTerminationStatus = TerminationStatus.early(phaseScope.getNextStepIndex());
break;
}
}
// We only store the termination status, which is exposed to the outside, when the phase has ended.
terminationStatus = translateEarlyTermination(phaseScope, earlyTerminationStatus, iterator.hasNext());
phaseEnded(phaseScope);
}

Expand Down Expand Up @@ -123,6 +141,7 @@ public void solvingStarted(SolverScope<Solution_> solverScope) {

public void phaseStarted(ConstructionHeuristicPhaseScope<Solution_> phaseScope) {
super.phaseStarted(phaseScope);
terminationStatus = TerminationStatus.NOT_TERMINATED;
entityPlacer.phaseStarted(phaseScope);
decider.phaseStarted(phaseScope);
}
Expand Down Expand Up @@ -150,6 +169,7 @@ public void stepEnded(ConstructionHeuristicStepScope<Solution_> stepScope) {

public void phaseEnded(ConstructionHeuristicPhaseScope<Solution_> phaseScope) {
super.phaseEnded(phaseScope);
ensureCorrectTermination(phaseScope, logger);
updateBestSolutionAndFire(phaseScope);
entityPlacer.phaseEnded(phaseScope);
decider.phaseEnded(phaseScope);
Expand Down Expand Up @@ -187,15 +207,15 @@ public void solvingError(SolverScope<Solution_> solverScope, Exception exception
}

public static class DefaultConstructionHeuristicPhaseBuilder<Solution_>
extends AbstractPhase.Builder<Solution_> {
extends AbstractPossiblyInitializingPhaseBuilder<Solution_> {

private final EntityPlacer<Solution_> entityPlacer;
private final ConstructionHeuristicDecider<Solution_> decider;

public DefaultConstructionHeuristicPhaseBuilder(int phaseIndex, boolean triggerFirstInitializedSolutionEvent,
public DefaultConstructionHeuristicPhaseBuilder(int phaseIndex, boolean lastInitializingPhase,
String logIndentation, Termination<Solution_> phaseTermination, EntityPlacer<Solution_> entityPlacer,
ConstructionHeuristicDecider<Solution_> decider) {
super(phaseIndex, triggerFirstInitializedSolutionEvent, logIndentation, phaseTermination);
super(phaseIndex, lastInitializingPhase, logIndentation, phaseTermination);
this.entityPlacer = entityPlacer;
this.decider = decider;
}
Expand All @@ -209,4 +229,5 @@ public DefaultConstructionHeuristicPhase<Solution_> build() {
return new DefaultConstructionHeuristicPhase<>(this);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public DefaultConstructionHeuristicPhaseFactory(ConstructionHeuristicPhaseConfig
}

public final DefaultConstructionHeuristicPhaseBuilder<Solution_> getBuilder(int phaseIndex,
boolean triggerFirstInitializedSolutionEvent, HeuristicConfigPolicy<Solution_> solverConfigPolicy,
boolean lastInitializingPhase, HeuristicConfigPolicy<Solution_> solverConfigPolicy,
Termination<Solution_> solverTermination) {
var constructionHeuristicType_ = Objects.requireNonNullElse(phaseConfig.getConstructionHeuristicType(),
ConstructionHeuristicType.ALLOCATE_ENTITY_FROM_QUEUE);
Expand All @@ -62,15 +62,14 @@ public final DefaultConstructionHeuristicPhaseBuilder<Solution_> getBuilder(int
.orElseGet(() -> buildDefaultEntityPlacerConfig(phaseConfigPolicy, constructionHeuristicType_));
var entityPlacer = EntityPlacerFactory.<Solution_> create(entityPlacerConfig_)
.buildEntityPlacer(phaseConfigPolicy);
return createBuilder(phaseConfigPolicy, solverTermination, phaseIndex, triggerFirstInitializedSolutionEvent,
entityPlacer);
return createBuilder(phaseConfigPolicy, solverTermination, phaseIndex, lastInitializingPhase, entityPlacer);
}

protected DefaultConstructionHeuristicPhaseBuilder<Solution_> createBuilder(
HeuristicConfigPolicy<Solution_> phaseConfigPolicy, Termination<Solution_> solverTermination, int phaseIndex,
boolean triggerFirstInitializedSolutionEvent, EntityPlacer<Solution_> entityPlacer) {
boolean lastInitializingPhase, EntityPlacer<Solution_> entityPlacer) {
var phaseTermination = buildPhaseTermination(phaseConfigPolicy, solverTermination);
var builder = new DefaultConstructionHeuristicPhaseBuilder<>(phaseIndex, triggerFirstInitializedSolutionEvent,
var builder = new DefaultConstructionHeuristicPhaseBuilder<>(phaseIndex, lastInitializingPhase,
phaseConfigPolicy.getLogIndentation(), phaseTermination, entityPlacer,
buildDecider(phaseConfigPolicy, phaseTermination));
var environmentMode = phaseConfigPolicy.getEnvironmentMode();
Expand All @@ -85,10 +84,10 @@ protected DefaultConstructionHeuristicPhaseBuilder<Solution_> createBuilder(
}

@Override
public ConstructionHeuristicPhase<Solution_> buildPhase(int phaseIndex, boolean triggerFirstInitializedSolutionEvent,
public ConstructionHeuristicPhase<Solution_> buildPhase(int phaseIndex, boolean lastInitializingPhase,
HeuristicConfigPolicy<Solution_> solverConfigPolicy, BestSolutionRecaller<Solution_> bestSolutionRecaller,
Termination<Solution_> solverTermination) {
return getBuilder(phaseIndex, triggerFirstInitializedSolutionEvent, solverConfigPolicy, solverTermination)
return getBuilder(phaseIndex, lastInitializingPhase, solverConfigPolicy, solverTermination)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public interface EntityPlacer<Solution_> extends Iterable<Placement<Solution_>>,
EntityPlacer<Solution_> rebuildWithFilter(SelectionFilter<Solution_, Object> filter);

EntityPlacer<Solution_> copy();

}
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public void solvingEnded(SolverScope<Solution_> solverScope) {
decider.solvingEnded(solverScope);
}

public static class Builder<Solution_> extends AbstractPhase.Builder<Solution_> {
public static class Builder<Solution_> extends AbstractPhaseBuilder<Solution_> {

private final Comparator<ExhaustiveSearchNode> nodeComparator;
private final EntitySelector<Solution_> entitySelector;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public DefaultExhaustiveSearchPhaseFactory(ExhaustiveSearchPhaseConfig phaseConf
}

@Override
public ExhaustiveSearchPhase<Solution_> buildPhase(int phaseIndex, boolean triggerFirstInitializedSolutionEvent,
public ExhaustiveSearchPhase<Solution_> buildPhase(int phaseIndex, boolean lastInitializingPhase,
HeuristicConfigPolicy<Solution_> solverConfigPolicy, BestSolutionRecaller<Solution_> bestSolutionRecaller,
Termination<Solution_> solverTermination) {
ExhaustiveSearchType exhaustiveSearchType_ = Objects.requireNonNullElse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package ai.timefold.solver.core.impl.heuristic.selector.move.generic;

import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import ai.timefold.solver.core.impl.constructionheuristic.ConstructionHeuristicPhase;
Expand All @@ -11,6 +13,9 @@
import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope;
import ai.timefold.solver.core.impl.solver.scope.SolverScope;

import org.jspecify.annotations.NullMarked;

@NullMarked
public final class RuinRecreateConstructionHeuristicPhase<Solution_>
extends DefaultConstructionHeuristicPhase<Solution_>
implements ConstructionHeuristicPhase<Solution_> {
Expand All @@ -21,7 +26,7 @@ public final class RuinRecreateConstructionHeuristicPhase<Solution_>

RuinRecreateConstructionHeuristicPhase(RuinRecreateConstructionHeuristicPhaseBuilder<Solution_> builder) {
super(builder);
this.elementsToRuinSet = builder.elementsToRuin;
this.elementsToRuinSet = Objects.requireNonNullElse(builder.elementsToRuin, Collections.emptySet());
this.missingUpdatedElementsMap = new IdentityHashMap<>();
}

Expand All @@ -42,7 +47,7 @@ public String getPhaseTypeString() {

@Override
protected void doStep(ConstructionHeuristicStepScope<Solution_> stepScope) {
if (elementsToRuinSet != null) {
if (!elementsToRuinSet.isEmpty()) {
var listVariableDescriptor = stepScope.getPhaseScope().getSolverScope().getSolutionDescriptor()
.getListVariableDescriptor();
var entity = stepScope.getStep().extractPlanningEntities().iterator().next();
Expand Down
Loading

0 comments on commit 6537668

Please sign in to comment.