Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions core/src/build/revapi-differences.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@
"old": "field ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_>>.subPillarSequenceComparatorClass",
"new": "field ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_>>.subPillarSequenceComparatorClass",
"justification": "Internal protected fields; safe."
},
{
"ignore": true,
"code": "java.annotation.attributeValueChanged",
"old": "class ai.timefold.solver.core.config.solver.SolverConfig",
"new": "class ai.timefold.solver.core.config.solver.SolverConfig",
"annotationType": "jakarta.xml.bind.annotation.XmlType",
"attribute": "propOrder",
"oldValue": "{\"enablePreviewFeatureSet\", \"environmentMode\", \"daemon\", \"randomSeed\", \"moveThreadCount\", \"moveThreadBufferSize\", \"threadFactoryClass\", \"monitoringConfig\", \"solutionClass\", \"entityClassList\", \"scoreDirectorFactoryConfig\", \"terminationConfig\", \"nearbyDistanceMeterClass\", \"phaseConfigList\"}",
"newValue": "{\"enablePreviewFeatureSet\", \"environmentMode\", \"daemon\", \"randomSeed\", \"moveThreadCount\", \"moveThreadBufferSize\", \"threadFactoryClass\", \"monitoringConfig\", \"solutionClass\", \"entityClassList\", \"scoreDirectorFactoryConfig\", \"terminationConfig\", \"nearbyDistanceMeterClass\", \"phaseConfigList\", \"reuseBestSolution\"}",
"justification": "add support for reusing the best solution"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"terminationConfig",
"nearbyDistanceMeterClass",
"phaseConfigList",
"reuseBestSolution",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need a new property. Do we always want to use the new related logic? Are there scenarios where enabling this feature by default may not be recommended?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Already discussed on Slack with Chris; no external configuration, we do the right thing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primary reason for the switch is that this reuses the existing solution; users might rely on getting new best solution instances if they save them outside the event handler (ex: store them in a MutableReference).

})
public final class SolverConfig extends AbstractConfig<SolverConfig> {

Expand Down Expand Up @@ -248,6 +249,8 @@ public final class SolverConfig extends AbstractConfig<SolverConfig> {
@XmlElement(name = "monitoring")
private MonitoringConfig monitoringConfig = null;

private Boolean reuseBestSolution = null;

// ************************************************************************
// Constructors and simple getters/setters
// ************************************************************************
Expand Down Expand Up @@ -448,6 +451,14 @@ public void setMonitoringConfig(@Nullable MonitoringConfig monitoringConfig) {
this.monitoringConfig = monitoringConfig;
}

public @Nullable Boolean getReuseBestSolution() {
return reuseBestSolution;
}

public void setReuseBestSolution(@Nullable Boolean reuseBestSolution) {
this.reuseBestSolution = reuseBestSolution;
}

// ************************************************************************
// With methods
// ************************************************************************
Expand Down Expand Up @@ -600,6 +611,11 @@ public void setMonitoringConfig(@Nullable MonitoringConfig monitoringConfig) {
return this;
}

public @NonNull SolverConfig withReuseBestSolution(@NonNull Boolean reuseBestSolution) {
this.reuseBestSolution = reuseBestSolution;
return this;
}

// ************************************************************************
// Smart getters
// ************************************************************************
Expand Down Expand Up @@ -677,6 +693,7 @@ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) {
inheritedConfig.nearbyDistanceMeterClass);
phaseConfigList = ConfigUtils.inheritMergeableListConfig(phaseConfigList, inheritedConfig.getPhaseConfigList());
monitoringConfig = ConfigUtils.inheritConfig(monitoringConfig, inheritedConfig.getMonitoringConfig());
reuseBestSolution = ConfigUtils.inheritOverwritableProperty(reuseBestSolution, inheritedConfig.getReuseBestSolution());
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
Expand Down Expand Up @@ -47,6 +48,7 @@
import ai.timefold.solver.core.impl.score.director.InnerScore;
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
import ai.timefold.solver.core.impl.solver.DefaultSolverFactory;
import ai.timefold.solver.core.impl.solver.recaller.ReusingBestSolutionUpdater;
import ai.timefold.solver.core.impl.solver.termination.PhaseTermination;
import ai.timefold.solver.core.impl.solver.termination.SolverTermination;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
Expand Down Expand Up @@ -210,6 +212,8 @@ Function<InnerScoreDirector<Solution_, Score_>, List<RecommendedAssignment<Out_,
Function<In_, @Nullable Out_> propositionFunction,
ScoreAnalysisFetchPolicy fetchPolicy);

<Solution_> ReusingBestSolutionUpdater<Solution_> buildReusingBestSolutionUpdater(ReadWriteLock readWriteLock);

enum Feature {
MULTITHREADED_SOLVING("Multi-threaded solving", "remove moveThreadCount from solver configuration"),
PARTITIONED_SEARCH("Partitioned search", "remove partitioned search phase from solver configuration"),
Expand All @@ -219,7 +223,8 @@ enum Feature {
"remove multistageMoveSelector and/or listMultistageMoveSelector from the solver configuration"),
CONSTRAINT_PROFILING("Constraint profiling", "remove constraintStreamProfilingEnabled from the solver configuration"),
SCORE_ANALYSIS("Score analysis", "do not use SolutionManager's analyze() method"),
RECOMMENDATIONS("Recommendations", "do not use SolutionManager's recommendAssignment() method");
RECOMMENDATIONS("Recommendations", "do not use SolutionManager's recommendAssignment() method"),
REUSE_BEST_SOLUTION("Reuse best solution", "remove reuseBestSolution from solver configuration");

private final String name;
private final String workaround;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class HeuristicConfigPolicy<Solution_> {
private final boolean unassignedValuesAllowed;
private final Class<? extends NearbyDistanceMeter<?, ?>> nearbyDistanceMeterClass;
private final RandomGenerator random;
private final boolean reuseBestSolution;

private final Map<String, EntityMimicRecorder<Solution_>> entityMimicRecorderMap = new HashMap<>();
private final Map<String, SubListMimicRecorder<Solution_>> subListMimicRecorderMap = new HashMap<>();
Expand All @@ -64,6 +65,7 @@ private HeuristicConfigPolicy(Builder<Solution_> builder) {
this.unassignedValuesAllowed = builder.unassignedValuesAllowed;
this.nearbyDistanceMeterClass = builder.nearbyDistanceMeterClass;
this.random = builder.random;
this.reuseBestSolution = builder.reuseBestSolution;
}

public EnvironmentMode getEnvironmentMode() {
Expand Down Expand Up @@ -122,6 +124,10 @@ public RandomGenerator getRandom() {
return random;
}

public boolean isReuseBestSolution() {
return reuseBestSolution;
}

// ************************************************************************
// Builder methods
// ************************************************************************
Expand All @@ -138,7 +144,8 @@ public Builder<Solution_> cloneBuilder() {
.withInitializingScoreTrend(initializingScoreTrend)
.withSolutionDescriptor(solutionDescriptor)
.withClassInstanceCache(classInstanceCache)
.withLogIndentation(logIndentation);
.withLogIndentation(logIndentation)
.withReuseBestSolution(reuseBestSolution);
}

public HeuristicConfigPolicy<Solution_> copyConfigPolicy() {
Expand Down Expand Up @@ -276,6 +283,7 @@ public static class Builder<Solution_> {

private Class<? extends NearbyDistanceMeter<?, ?>> nearbyDistanceMeterClass;
private RandomGenerator random;
private boolean reuseBestSolution = false;

public Builder<Solution_> withPreviewFeatureSet(Set<PreviewFeature> previewFeatureSet) {
this.previewFeatureSet = previewFeatureSet;
Expand Down Expand Up @@ -353,6 +361,11 @@ public Builder<Solution_> withUnassignedValuesAllowed(boolean unassignedValuesAl
return this;
}

public Builder<Solution_> withReuseBestSolution(boolean reuseBestSolution) {
this.reuseBestSolution = reuseBestSolution;
return this;
}

public HeuristicConfigPolicy<Solution_> build() {
return new HeuristicConfigPolicy<>(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ protected void doStep(LocalSearchStepScope<Solution_> stepScope) {
stepScope.getScoreDirector().executeMove(step);
predictWorkingStepScore(stepScope, step);
var solver = stepScope.getPhaseScope().getSolverScope().getSolver();
solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope);
solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope,
stepScope.getPhaseScope().getAcceptedMoveList());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ The move repository (%s) is neverEnding (%s), but the forager (%s) does not supp
var moveThreadCount = configPolicy.getMoveThreadCount();
var environmentMode = configPolicy.getEnvironmentMode();
var decider = moveThreadCount == null
? new LocalSearchDecider<>(configPolicy.getLogIndentation(), termination, moveRepository, acceptor, forager)
? new LocalSearchDecider<>(configPolicy.getLogIndentation(), termination, moveRepository, acceptor, forager,
configPolicy.isReuseBestSolution())
: TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTITHREADED_SOLVING)
.buildLocalSearch(moveThreadCount, termination, moveRepository, acceptor, forager, environmentMode,
configPolicy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,20 @@ public class LocalSearchDecider<Solution_> {
protected final MoveRepository<Solution_> moveRepository;
protected final Acceptor<Solution_> acceptor;
protected final LocalSearchForager<Solution_> forager;
protected final boolean reuseBestSolution;

protected boolean assertMoveScoreFromScratch = false;
protected boolean assertExpectedUndoMoveScore = false;

public LocalSearchDecider(String logIndentation, PhaseTermination<Solution_> termination,
MoveRepository<Solution_> moveRepository, Acceptor<Solution_> acceptor, LocalSearchForager<Solution_> forager) {
MoveRepository<Solution_> moveRepository, Acceptor<Solution_> acceptor, LocalSearchForager<Solution_> forager,
boolean reuseBestSolution) {
this.logIndentation = logIndentation;
this.termination = termination;
this.moveRepository = moveRepository;
this.acceptor = acceptor;
this.forager = forager;
this.reuseBestSolution = reuseBestSolution;
}

public Termination<Solution_> getTermination() {
Expand Down Expand Up @@ -137,6 +140,9 @@ protected void pickMove(LocalSearchStepScope<Solution_> stepScope) {
stepScope.setStepString(step.toString());
}
stepScope.setScore(pickedMoveScope.getScore());
if (reuseBestSolution) {
stepScope.getPhaseScope().addAcceptedMove(step);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package ai.timefold.solver.core.impl.localsearch.scope;

import java.util.ArrayList;
import java.util.List;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
import ai.timefold.solver.core.preview.api.move.Move;

/**
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
public final class LocalSearchPhaseScope<Solution_> extends AbstractPhaseScope<Solution_> {

private LocalSearchStepScope<Solution_> lastCompletedStepScope;
private final List<Move<Solution_>> acceptedMoveList = new ArrayList<>();

public LocalSearchPhaseScope(SolverScope<Solution_> solverScope, int phaseIndex) {
super(solverScope, phaseIndex);
Expand All @@ -26,6 +31,14 @@ public void setLastCompletedStepScope(LocalSearchStepScope<Solution_> lastComple
this.lastCompletedStepScope = lastCompletedStepScope;
}

public List<Move<Solution_>> getAcceptedMoveList() {
return acceptedMoveList;
}

public void addAcceptedMove(Move<Solution_> move) {
acceptedMoveList.add(move);
}

// ************************************************************************
// Calculated methods
// ************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.jspecify.annotations.NullMarked;

@NullMarked
final class MoveTesterScoreDirectorFactory<Solution_, Score_ extends Score<Score_>>
public final class MoveTesterScoreDirectorFactory<Solution_, Score_ extends Score<Score_>>
extends AbstractScoreDirectorFactory<Solution_, Score_, MoveTesterScoreDirectorFactory<Solution_, Score_>> {

public MoveTesterScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, EnvironmentMode environmentMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
Expand Down Expand Up @@ -66,33 +68,36 @@ public ConsumerSupport(ProblemId_ problemId, @Nullable Consumer<NewBestSolutionE
}

void consumeIntermediateBestSolution(Solution_ solution, EventProducerId producerId,
BooleanSupplier isEveryProblemChangeProcessed) {
BooleanSupplier isEveryProblemChangeProcessed, Lock lock) {
/*
* If the bestSolutionConsumer is not provided, the best solution is still set for the purpose of recording
* problem changes.
*/
bestSolutionHolder.set(solution, producerId, isEveryProblemChangeProcessed);
if (bestSolutionConsumer != null) {
tryConsumeWaitingIntermediateBestSolution();
tryConsumeWaitingIntermediateBestSolution(lock);
}
}

// Called both on the Solver thread and the Consumer thread.
private void tryConsumeWaitingIntermediateBestSolution() {
private void tryConsumeWaitingIntermediateBestSolution(Lock lock) {
if (bestSolutionHolder.isEmpty()) {
return; // There is no best solution to consume.
}
if (activeConsumption.tryAcquire()) {
scheduleIntermediateBestSolutionConsumption()
scheduleIntermediateBestSolutionConsumption(lock)
.whenCompleteAsync((solution, throwable) -> {
activeConsumption.release();
tryConsumeWaitingIntermediateBestSolution();
tryConsumeWaitingIntermediateBestSolution(lock);
}, consumerExecutor);
}
}

private CompletableFuture<Void> scheduleIntermediateBestSolutionConsumption() {
private CompletableFuture<Void> scheduleIntermediateBestSolutionConsumption(Lock lock) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe here is another place where we should consider keeping the existing contract unchanged. Let's say we can create LockableConsumerSupport that receives or creates its internal lock instance. We could extend ConsumerSupport and override scheduleIntermediateBestSolutionConsumption without changing the existing contract. Does it make sense?

return CompletableFuture.runAsync(() -> {
// If reuse best solution is enabled, this will block if the best solution
// is currently being updated.
lock.lock();
var bestSolutionContainingProblemChanges = bestSolutionHolder.take();
if (bestSolutionContainingProblemChanges != null) {
try {
Expand All @@ -107,6 +112,7 @@ private CompletableFuture<Void> scheduleIntermediateBestSolutionConsumption() {
bestSolutionContainingProblemChanges.completeProblemChangesExceptionally(throwable);
}
}
lock.unlock();
}, consumerExecutor);
}

Expand Down Expand Up @@ -175,7 +181,7 @@ void consumeFinalBestSolution(Solution_ solution) { // Called on the Solver thre
// Situation:
// The consumer is consuming the last but one best solution. The final best solution is waiting for the consumer.
if (bestSolutionConsumer != null) {
scheduleIntermediateBestSolutionConsumption();
scheduleIntermediateBestSolutionConsumption(new ReentrantLock());
}
scheduleFinalBestSolutionConsumption(solution)
.whenComplete((unused, throwable) -> releaseAll());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ public Solver<Solution_> buildSolver(SolverConfigOverride configOverride) {
solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(castScoreDirector));

var moveThreadCount = resolveMoveThreadCount(true);
var bestSolutionRecaller = BestSolutionRecallerFactory.create().<Solution_> buildBestSolutionRecaller(environmentMode);
var bestSolutionRecaller = BestSolutionRecallerFactory.create().<Solution_> buildBestSolutionRecaller(environmentMode,
solverConfig.getReuseBestSolution());
var randomFactory = buildRandomSupplier(environmentMode);
var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet();

Expand All @@ -157,6 +158,7 @@ public Solver<Solution_> buildSolver(SolverConfigOverride configOverride) {
.withInitializingScoreTrend(scoreDirectorFactory.getInitializingScoreTrend())
.withSolutionDescriptor(solutionDescriptor)
.withClassInstanceCache(ClassInstanceCache.create())
.withReuseBestSolution(Objects.requireNonNullElse(solverConfig.getReuseBestSolution(), false))
.build();
var basicPlumbingTermination = new BasicPlumbingTermination<Solution_>(isDaemon);
var termination = buildTermination(basicPlumbingTermination, configPolicy, configOverride);
Expand Down
Loading
Loading