Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: log a warning when CH terminated prematurely #1362

Merged
merged 10 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
triceo marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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
Loading