Skip to content

Commit

Permalink
feat: log a warning when CH terminated prematurely
Browse files Browse the repository at this point in the history
  • Loading branch information
triceo committed Jan 28, 2025
1 parent 9f53d4a commit b5b29b4
Show file tree
Hide file tree
Showing 23 changed files with 274 additions and 93 deletions.
27 changes: 20 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,16 @@ 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.
* <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 +188,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 @@ -77,9 +77,12 @@ 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.
* 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.
* The consumer will not be called if this phase terminated prematurely,
* in which case the solution is not fully initialized.
*
* @param firstInitializedSolutionConsumer called only once before starting the first Local Search phase
* @return this
Expand Down
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 @@ -9,29 +9,45 @@
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 AbstractPhase<Solution_>
implements ConstructionHeuristicPhase<Solution_> {

protected final ConstructionHeuristicDecider<Solution_> decider;
protected final EntityPlacer<Solution_> entityPlacer;
private final boolean lastInitializingPhase;
private TerminationStatus terminationStatus = TerminationStatus.NOT_STARTED;

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

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

@Override
public boolean isLastInitializingPhase() {
return lastInitializingPhase;
}

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

@Override
public String getPhaseTypeString() {
return "Construction Heuristics";
Expand Down Expand Up @@ -83,17 +99,26 @@ 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.
terminationStatus = 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) {
terminationStatus = TerminationStatus.regular(phaseScope.getNextStepIndex());
break;
} else if (phaseTermination.isPhaseTerminated(phaseScope)) {
terminationStatus = TerminationStatus.early(phaseScope.getNextStepIndex());
break;
}
}
if (!terminationStatus.terminated()) {
// The phase is over, yet status is not terminated yet.
// This means we need to set the termination status to indicate that the phase has ended successfully.
terminationStatus = TerminationStatus.regular(phaseScope.getNextStepIndex());
}
phaseEnded(phaseScope);
}

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

public void phaseStarted(ConstructionHeuristicPhaseScope<Solution_> phaseScope) {
super.phaseStarted(phaseScope);
terminationStatus = TerminationStatus.NOT_STARTED;
entityPlacer.phaseStarted(phaseScope);
decider.phaseStarted(phaseScope);
}
Expand Down Expand Up @@ -150,6 +176,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 @@ -192,10 +219,10 @@ public static class DefaultConstructionHeuristicPhaseBuilder<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 +236,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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class RuinRecreateConstructionHeuristicPhaseFactory<Solution_>
@Override
protected DefaultConstructionHeuristicPhaseBuilder<Solution_> createBuilder(
HeuristicConfigPolicy<Solution_> phaseConfigPolicy,
Termination<Solution_> solverTermination, int phaseIndex, boolean triggerFirstInitializedSolutionEvent,
Termination<Solution_> solverTermination, int phaseIndex, boolean lastInitializingPhase,
EntityPlacer<Solution_> entityPlacer) {
var phaseTermination = new PhaseToSolverTerminationBridge<>(new BasicPlumbingTermination<Solution_>(false));
return new RuinRecreateConstructionHeuristicPhaseBuilder<>(phaseConfigPolicy, this, phaseTermination, entityPlacer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public DefaultLocalSearchPhaseFactory(LocalSearchPhaseConfig phaseConfig) {
}

@Override
public LocalSearchPhase<Solution_> buildPhase(int phaseIndex, boolean triggerFirstInitializedSolutionEvent,
public LocalSearchPhase<Solution_> buildPhase(int phaseIndex, boolean lastInitializingPhase,
HeuristicConfigPolicy<Solution_> solverConfigPolicy, BestSolutionRecaller<Solution_> bestSolutionRecaller,
Termination<Solution_> solverTermination) {
var phaseConfigPolicy = solverConfigPolicy.createPhaseConfigPolicy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public DefaultPartitionedSearchPhaseFactory(PartitionedSearchPhaseConfig phaseCo
}

@Override
public PartitionedSearchPhase<Solution_> buildPhase(int phaseIndex, boolean triggerFirstInitializedSolutionEvent,
public PartitionedSearchPhase<Solution_> buildPhase(int phaseIndex, boolean lastInitializingPhase,
HeuristicConfigPolicy<Solution_> solverConfigPolicy, BestSolutionRecaller<Solution_> bestSolutionRecaller,
Termination<Solution_> solverTermination) {
return TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.PARTITIONED_SEARCH)
Expand Down
Loading

0 comments on commit b5b29b4

Please sign in to comment.