Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class SimulatedAnnealingAcceptor<Solution_> extends AbstractAcceptor<Solu
protected double[] temperatureLevels;

protected double temperatureMinimum = 1.0E-100; // Double.MIN_NORMAL is E-308
private boolean noTimeGradientWarned;

public void setStartingTemperature(Score startingTemperature) {
this.startingTemperature = startingTemperature;
Expand All @@ -40,6 +41,7 @@ public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
startingTemperatureLevels = startingTemperature.toLevelDoubles();
temperatureLevels = startingTemperatureLevels;
levelsLength = startingTemperatureLevels.length;
noTimeGradientWarned = false;
}

@Override
Expand Down Expand Up @@ -83,6 +85,16 @@ public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
super.stepStarted(stepScope);
// TimeGradient only refreshes at the beginning of a step, so this code is in stepStarted instead of stepEnded
var timeGradient = stepScope.getTimeGradient();
if (timeGradient < 0.0) {
// Without a known end, such as when only terminateEarly() stops the solver, it keeps the starting temperature.
if (!noTimeGradientWarned) {
logger.warn("""
Simulated annealing does not cool, because no termination can estimate how long the phase will continue.
Maybe configure a time limit or a diminished returns termination.""");
noTimeGradientWarned = true;
}
timeGradient = 0.0;
}
var reverseTimeGradient = 1.0 - timeGradient;
temperatureLevels = new double[levelsLength];
for (var i = 0; i < levelsLength; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,13 @@ protected PhaseTermination<Solution_> buildPhaseTermination(HeuristicConfigPolic
var phaseName = this.getClass().getSimpleName()
.replace("PhaseFactory", "")
.replace("Default", "");
if (solverTermination != resultingTermination) {
// Only fail if the user put the inapplicable termination on the phase, not on the solver.
// On the solver level, inapplicable phase terminations are skipped.
// Otherwise you would only be able to configure a global phase-level termination on the solver
// if it was applicable to all phases.
if (!inapplicableTerminationList.isEmpty()) {
throw new IllegalStateException(
"""
The phase (%s) configured with terminations (%s) includes some terminations which are not applicable to it (%s).
Maybe remove these terminations from the phase's configuration."""
.formatted(phaseName, phaseTermination, inapplicableTerminationList));
}
} else if (!inapplicableTerminationList.isEmpty()) {
logger.trace("""
The solver-level termination ({}) includes phase-level terminations ({}) \
which are not applicable to the phase ({}).
These phase-level terminations will not take effect in this phase.""",
solverTermination, inapplicableTerminationList, phaseName);
// Inapplicable solver-level terminations stay behind the bridge, which never counts them as met in this phase.
if (!inapplicableTerminationList.isEmpty()) {
throw new IllegalStateException(
"""
The phase (%s) configured with terminations (%s) includes some terminations which are not applicable to it (%s).
Maybe remove these terminations from the phase's configuration."""
.formatted(phaseName, phaseTermination, inapplicableTerminationList));
}
return resultingTermination;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import ai.timefold.solver.core.impl.exhaustivesearch.DefaultExhaustiveSearchPhaseFactory;
import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy;
import ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhaseFactory;
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
import ai.timefold.solver.core.impl.partitionedsearch.DefaultPartitionedSearchPhaseFactory;
import ai.timefold.solver.core.impl.phase.custom.DefaultCustomPhaseFactory;
import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller;
import ai.timefold.solver.core.impl.solver.termination.SolverTermination;
import ai.timefold.solver.core.impl.solver.termination.UniversalTermination;

public interface PhaseFactory<Solution_> {

Expand Down Expand Up @@ -48,7 +50,7 @@ static <Solution_> List<Phase<Solution_>> buildPhases(List<PhaseConfig> phaseCon
var phaseConfig = phaseConfigList.get(phaseIndex);
if (phaseIndex > 0) {
PhaseConfig previousPhaseConfig = phaseConfigList.get(phaseIndex - 1);
if (!canTerminate(previousPhaseConfig)) {
if (!canTerminate(previousPhaseConfig) && !canSolverTerminationEndPhase(termination, previousPhaseConfig)) {
throw new IllegalStateException("Solver configuration contains an unreachable phase. "
+ "Phase #" + phaseIndex + " (" + phaseConfig + ") follows a phase "
+ "without a configured termination (" + previousPhaseConfig + ").");
Expand Down Expand Up @@ -86,6 +88,19 @@ static <Solution_> List<Phase<Solution_>> buildPhases(List<PhaseConfig> phaseCon
return phaseList;
}

/**
* A phase-only termination in the solver termination ends the phase but not the solver,
* so the next phase runs.
*/
private static <Solution_> boolean canSolverTerminationEndPhase(SolverTermination<Solution_> termination,
PhaseConfig<?> phaseConfig) {
return phaseConfig instanceof LocalSearchPhaseConfig
&& termination instanceof UniversalTermination<Solution_> universalTermination
&& universalTermination.getPhaseTerminationList().stream()
.anyMatch(phaseTermination -> !(phaseTermination instanceof SolverTermination)
&& phaseTermination.isApplicableTo(LocalSearchPhaseScope.class));
}

static boolean canTerminate(PhaseConfig phaseConfig) {
if (phaseConfig instanceof ConstructionHeuristicPhaseConfig
|| phaseConfig instanceof ExhaustiveSearchPhaseConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,53 @@ public final List<PhaseTermination<Solution_>> getPhaseTerminationList() {
return List.copyOf(result);
}

/**
* Whether this solver termination ends the phase:
* solver terminations measure the solver, phase-only terminations the phase that they can end.
* A phase-only termination that is not applicable to the phase counts as not terminated.
*/
abstract boolean isSolverTerminatedForPhase(AbstractPhaseScope<Solution_> phaseScope);

static <Solution_> boolean isTerminatedForPhase(Termination<Solution_> termination,
AbstractPhaseScope<Solution_> phaseScope) {
return switch (termination) {
case AbstractCompositeTermination<Solution_> compositeTermination -> compositeTermination
.isSolverTerminatedForPhase(phaseScope);
case SolverTermination<Solution_> solverTermination -> solverTermination
.isSolverTerminated(phaseScope.getSolverScope());
case PhaseTermination<Solution_> phaseTermination -> phaseTermination.isApplicableTo(phaseScope.getClass())
&& phaseTermination.isPhaseTerminated(phaseScope);
default -> throw new IllegalStateException("Impossible state: the termination (%s) is of an unknown type."
.formatted(termination));
};
}

/**
* The timeGradient of this solver termination, as a phase sees it:
* solver terminations measure the solver, phase-only terminations the phase that they can end.
*
* @return -1.0 if none of the terminations supports a timeGradient
*/
final double calculateSolverTimeGradientForPhase(AbstractPhaseScope<Solution_> phaseScope) {
var timeGradient = -1.0;
for (var termination : terminationList) {
var nextTimeGradient = switch (termination) {
case AbstractCompositeTermination<Solution_> compositeTermination -> compositeTermination
.calculateSolverTimeGradientForPhase(phaseScope);
case SolverTermination<Solution_> solverTermination -> solverTermination
.calculateSolverTimeGradient(phaseScope.getSolverScope());
case PhaseTermination<Solution_> phaseTermination when phaseTermination
.isApplicableTo(phaseScope.getClass()) ->
phaseTermination.calculatePhaseTimeGradient(phaseScope);
default -> -1.0;
};
if (nextTimeGradient >= 0.0) {
timeGradient = timeGradient < 0.0 ? nextTimeGradient : combineTimeGradients(timeGradient, nextTimeGradient);
}
}
return timeGradient;
}

abstract double combineTimeGradients(double timeGradient, double otherTimeGradient);

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ public AndCompositeTermination(Termination<Solution_>... terminations) {
}

/**
* @return true if all terminations are terminated.
* Only a phase can check the terminations that can only end a phase,
* see {@link #isSolverTerminatedForPhase(AbstractPhaseScope)}.
*
* @return true if all terminations are terminated;
* false if this includes a termination that can only end a phase.
*/
@Override
public boolean isSolverTerminated(SolverScope<Solution_> solverScope) {
if (solverTerminationList.size() != terminationList.size()) {
return false;
}
for (var termination : solverTerminationList) {
if (!termination.isSolverTerminated(solverScope)) {
return false;
Expand All @@ -48,19 +55,29 @@ public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {
return true;
}

@Override
boolean isSolverTerminatedForPhase(AbstractPhaseScope<Solution_> phaseScope) {
for (var termination : terminationList) {
if (!isTerminatedForPhase(termination, phaseScope)) {
return false;
}
}
return true;
}

/**
* Calculates the minimum timeGradient of all Terminations.
* Not supported timeGradients (-1.0) are ignored.
*
* @return the minimum timeGradient of the Terminations.
* @return the minimum timeGradient of the Terminations, or -1.0 if none of them supports it.
*/
@Override
public double calculateSolverTimeGradient(SolverScope<Solution_> solverScope) {
var timeGradient = 1.0;
var timeGradient = -1.0;
for (var termination : solverTerminationList) {
var nextTimeGradient = termination.calculateSolverTimeGradient(solverScope);
if (nextTimeGradient >= 0.0) {
timeGradient = Math.min(timeGradient, nextTimeGradient);
timeGradient = timeGradient < 0.0 ? nextTimeGradient : Math.min(timeGradient, nextTimeGradient);
}
}
return timeGradient;
Expand All @@ -70,23 +87,28 @@ public double calculateSolverTimeGradient(SolverScope<Solution_> solverScope) {
* Calculates the minimum timeGradient of all Terminations.
* Not supported timeGradients (-1.0) are ignored.
*
* @return the minimum timeGradient of the Terminations.
* @return the minimum timeGradient of the Terminations, or -1.0 if none of them supports it.
*/
@Override
public double calculatePhaseTimeGradient(AbstractPhaseScope<Solution_> phaseScope) {
var timeGradient = 1.0;
var timeGradient = -1.0;
for (var termination : phaseTerminationList) {
if (!termination.isApplicableTo(phaseScope.getClass())) {
continue;
}
var nextTimeGradient = termination.calculatePhaseTimeGradient(phaseScope);
if (nextTimeGradient >= 0.0) {
timeGradient = Math.min(timeGradient, nextTimeGradient);
timeGradient = timeGradient < 0.0 ? nextTimeGradient : Math.min(timeGradient, nextTimeGradient);
}
}
return timeGradient;
}

@Override
double combineTimeGradients(double timeGradient, double otherTimeGradient) {
return Math.min(timeGradient, otherTimeGradient);
}

@Override
public AndCompositeTermination<Solution_> createChildThreadTermination(SolverScope<Solution_> solverScope,
ChildThreadType childThreadType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,22 @@ public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {

@Override
public double calculatePhaseTimeGradient(AbstractPhaseScope<Solution_> phaseScope) {
return -1.0;
return calculateTimeGradient(System.nanoTime());
Comment thread
triceo marked this conversation as resolved.
Resolved
}

/**
* Assumes that one sliding window is left after the time since the grace period started,
* which a change of a harder level restarts.
*/
double calculateTimeGradient(long currentTime) {
Comment thread
triceo marked this conversation as resolved.
Resolved
if (!isGracePeriodStarted) {
return 0.0;
}
var timeSinceGracePeriodStartNanos = currentTime - gracePeriodStartTimeNanos;
if (timeSinceGracePeriodStartNanos <= 0L) {
return 0.0;
}
return timeSinceGracePeriodStartNanos / (double) (timeSinceGracePeriodStartNanos + slidingWindowNanos);
}

@Override
Expand Down Expand Up @@ -190,7 +205,14 @@ public void stepStarted(AbstractStepScope<Solution_> stepScope) {

@Override
public void stepEnded(AbstractStepScope<Solution_> stepScope) {
step(System.nanoTime(), stepScope.getPhaseScope().getBestScore());
var phaseScope = stepScope.getPhaseScope();
var currentTime = System.nanoTime();
var bestScore = phaseScope.<Score_> getBestScore();
step(currentTime, bestScore);
if (isApplicableTo(phaseScope.getClass())) {
// Restarts the grace period on time, also when a composite such as AND skips isPhaseTerminated().
isTerminated(currentTime, bestScore);
}
}

@SuppressWarnings("rawtypes")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ private void recordTermination(SolverScope<Solution_> solverScope, Termination<S
}

/**
* Only a phase can check the terminations that can only end a phase,
* see {@link #isSolverTerminatedForPhase(AbstractPhaseScope)}.
*
* @param solverScope never null
* @return true if any one of the terminations is terminated.
* @return true if any one of the solver terminations is terminated.
*/
@Override
public boolean isSolverTerminated(SolverScope<Solution_> solverScope) {
Expand Down Expand Up @@ -90,15 +93,26 @@ public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {
return false;
}

@Override
boolean isSolverTerminatedForPhase(AbstractPhaseScope<Solution_> phaseScope) {
for (var termination : terminationList) {
if (isTerminatedForPhase(termination, phaseScope)) {
recordTermination(phaseScope.getSolverScope(), termination);
return true;
}
}
return false;
}

/**
* Calculates the maximum timeGradient of all Terminations.
* Not supported timeGradients (-1.0) are ignored.
*
* @return the maximum timeGradient of the terminations.
* @return the maximum timeGradient of the terminations, or -1.0 if none of them supports it.
*/
@Override
public double calculateSolverTimeGradient(SolverScope<Solution_> solverScope) {
var timeGradient = 0.0;
var timeGradient = -1.0;
for (var termination : solverTerminationList) {
var nextTimeGradient = termination.calculateSolverTimeGradient(solverScope);
if (nextTimeGradient >= 0.0) {
Expand All @@ -112,11 +126,11 @@ public double calculateSolverTimeGradient(SolverScope<Solution_> solverScope) {
* Calculates the maximum timeGradient of all Terminations.
* Not supported timeGradients (-1.0) are ignored.
*
* @return the maximum timeGradient of the supported terminations.
* @return the maximum timeGradient of the supported terminations, or -1.0 if none of them supports it.
*/
@Override
public double calculatePhaseTimeGradient(AbstractPhaseScope<Solution_> phaseScope) {
var timeGradient = 0.0;
var timeGradient = -1.0;
for (var termination : phaseTerminationList) {
if (!termination.isApplicableTo(phaseScope.getClass())) {
continue;
Expand All @@ -129,6 +143,11 @@ public double calculatePhaseTimeGradient(AbstractPhaseScope<Solution_> phaseScop
return timeGradient;
}

@Override
double combineTimeGradients(double timeGradient, double otherTimeGradient) {
return Math.max(timeGradient, otherTimeGradient);
}

@Override
public OrCompositeTermination<Solution_> createChildThreadTermination(SolverScope<Solution_> solverScope,
ChildThreadType childThreadType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
* The effect of this in practice would have been that,
* if a solver-level {@link TimeMillisSpentTermination} were configured to terminate after 10 seconds,
* each phase would effectively start a new 10-second counter.
* <p>
* A composite solver termination can also include terminations that can only end a phase.
* Each of its terminations is checked at its own level:
* solver terminations against the solver, the other terminations against the phase.
* The time gradient follows the same rule.
*
* @param <Solution_>
*/
Expand All @@ -39,6 +44,9 @@ public SolverBridgePhaseTermination(SolverTermination<Solution_> solverTerminati
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {
if (solverTermination instanceof AbstractCompositeTermination<Solution_> compositeTermination) {
return compositeTermination.isSolverTerminatedForPhase(phaseScope);
}
var terminated = solverTermination.isSolverTerminated(phaseScope.getSolverScope());
// If the solver is not finished yet, we need to check the phase termination
if (!terminated && solverTermination instanceof PhaseTermination phaseTermination) {
Expand All @@ -49,6 +57,10 @@ public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {

@Override
public double calculatePhaseTimeGradient(AbstractPhaseScope<Solution_> phaseScope) {
if (solverTermination instanceof AbstractCompositeTermination<Solution_> compositeTermination) {
Comment thread
triceo marked this conversation as resolved.
Resolved
// Its phase-only terminations can end the phase too.
return compositeTermination.calculateSolverTimeGradientForPhase(phaseScope);
}
return solverTermination.calculateSolverTimeGradient(phaseScope.getSolverScope());
}

Expand Down
Loading
Loading