Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ai.timefold.solver.core.api.domain.common;

import ai.timefold.solver.core.api.solver.change.ProblemChange;

import org.jspecify.annotations.Nullable;

/**
* Allows to transfer an entity or fact instance (often from another {@link Thread})
* to another working solution.
*/
public interface Lookup {

/**
* Translates an entity or fact instance (often from another {@link Thread})
* to another working solution.
* Useful for move rebasing and in a {@link ProblemChange} and for multi-threaded solving.
* <p>
* Matching uses {@link PlanningId}.
*
* @param problemFactOrPlanningEntity The fact or entity to rebase.
* @return null if problemFactOrPlanningEntity is null
* @throws IllegalArgumentException if there is no working object for the fact or entity,
* if it cannot be looked up,
* or if its class is not supported.
* @throws IllegalStateException if it cannot be looked up
* @param <T> the object type
*/
<T> @Nullable T lookUpWorkingObjectOrFail(@Nullable T problemFactOrPlanningEntity);

/**
* As defined by {@link #lookUpWorkingObjectOrFail(Object)},
* but doesn't fail fast if no workingObject was ever added for the externalObject.
* It's recommended to use {@link #lookUpWorkingObjectOrFail(Object)} instead,
* especially in move rebasing code.
*
* @return null if externalObject is null, or if there is no workingObject for externalObject
* @throws IllegalArgumentException if it cannot be looked up or if the externalObject's class is not supported
* @throws IllegalStateException if it cannot be looked up
* @param <T> the object type
*/
<T> @Nullable T lookUpWorkingObject(@Nullable T externalObject);

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.score.director.ScoreDirector;
import ai.timefold.solver.core.api.solver.change.ProblemChange;
import ai.timefold.solver.core.preview.api.move.Move;

/**
* Specifies that a bean property (or a field) is the id to match
* when {@link ScoreDirector#lookUpWorkingObject(Object) locating}
* when {@link Lookup#lookUpWorkingObjectOrFail(Object) locating}
* an externalObject (often from another {@link Thread} or JVM).
* Used during {@link Move} rebasing and in a {@link ProblemChange}.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@
import java.util.SortedSet;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.score.director.ScoreDirector;

/**
* Specifies that a property (or a field) on a {@link PlanningSolution} class is a {@link Collection} of planning entities.
* <p>
* Every element in the planning entity collection should have the {@link PlanningEntity} annotation.
* Every element in the planning entity collection will be added to the {@link ScoreDirector}.
* Every element in the planning entity collection will be registered with the solver.
* <p>
* For solver reproducibility, the collection must have a deterministic, stable iteration order.
* It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@
import java.lang.annotation.Target;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.score.director.ScoreDirector;

/**
* Specifies that a property (or a field) on a {@link PlanningSolution} class is a planning entity.
* <p>
* The planning entity should have the {@link PlanningEntity} annotation.
* The planning entity will be added to the {@link ScoreDirector}.
* The planning entity will be registered with the solver.
*/
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package ai.timefold.solver.core.api.solver.change;

import java.util.Optional;
import java.util.function.Consumer;

import ai.timefold.solver.core.api.domain.common.Lookup;
import ai.timefold.solver.core.api.domain.common.PlanningId;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
Expand All @@ -21,7 +21,8 @@
* To see an example implementation, please refer to the {@link ProblemChange} Javadoc.
*/
@NullMarked
public interface ProblemChangeDirector {
public interface ProblemChangeDirector
extends Lookup {

/**
* Add a new {@link PlanningEntity} instance into the {@link PlanningSolution working solution}.
Expand Down Expand Up @@ -100,19 +101,21 @@ <EntityOrProblemFact> void changeProblemProperty(EntityOrProblemFact problemFact
* @throws IllegalStateException if it cannot be looked up
* @param <EntityOrProblemFact> the object type
*/
@Override
<EntityOrProblemFact> @Nullable EntityOrProblemFact lookUpWorkingObjectOrFail(@Nullable EntityOrProblemFact externalObject);

/**
* As defined by {@link #lookUpWorkingObjectOrFail(Object)},
* but doesn't fail fast if no workingObject was ever added for the externalObject.
* It's recommended to use {@link #lookUpWorkingObjectOrFail(Object)} instead.
*
* @return {@link Optional#empty()} if there is no workingObject for externalObject, or if externalObject is null
* @param <EntityOrProblemFact> the object type
* @return null if there is no workingObject for externalObject, or if externalObject is null
* @throws IllegalArgumentException if it cannot be looked up or if the externalObject's class is not supported
* @throws IllegalStateException if it cannot be looked up
* @param <EntityOrProblemFact> the object type
*/
<EntityOrProblemFact> Optional<EntityOrProblemFact> lookUpWorkingObject(@Nullable EntityOrProblemFact externalObject);
@Override
<EntityOrProblemFact> @Nullable EntityOrProblemFact lookUpWorkingObject(@Nullable EntityOrProblemFact externalObject);

/**
* Calls variable listeners on the external changes submitted so far.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,34 @@
package ai.timefold.solver.core.api.solver.phase;

import java.util.function.BooleanSupplier;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.director.ScoreDirector;
import ai.timefold.solver.core.api.solver.Solver;
import ai.timefold.solver.core.api.solver.change.ProblemChange;
import ai.timefold.solver.core.impl.phase.Phase;
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
import ai.timefold.solver.core.preview.api.move.Move;

import org.jspecify.annotations.NullMarked;

/**
* Runs a custom algorithm as a {@link Phase} of the {@link Solver} that changes the planning variables.
* To change problem facts, use {@link Solver#addProblemChange(ProblemChange)} instead.
* <p>
* To add custom properties, configure custom properties and add public setters for them.
* To change problem facts and to add or remove entities, use {@link Solver#addProblemChange(ProblemChange)} instead.
*
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
@NullMarked
public interface PhaseCommand<Solution_> {

/**
* Changes {@link PlanningSolution working solution} of {@link ScoreDirector#getWorkingSolution()}.
* When the {@link PlanningSolution working solution} is modified,
* the {@link ScoreDirector} must be correctly notified
* (through {@link ScoreDirector#beforeVariableChanged(Object, String)} and
* {@link ScoreDirector#afterVariableChanged(Object, String)}),
* otherwise calculated {@link Score}s will be corrupted.
* Changes the current {@link PhaseCommandContext#getWorkingSolution() working solution}.
* The solver is notified of the changes through {@link PhaseCommandContext},
* specifically through {@link PhaseCommandContext#execute(Move)}.
* Any other modifications to the working solution are strictly forbidden
* and will likely cause the solver to be in an inconsistent state and throw an exception later on.
* <p>
* Don't forget to call {@link ScoreDirector#triggerVariableListeners()} after each set of changes
* (especially before every {@link InnerScoreDirector#calculateScore()} call)
* to ensure all shadow variables are updated.
* Don't forget to check {@link PhaseCommandContext#isPhaseTerminated() termination status} frequently
* to allow the solver to gracefully terminate when necessary.
*
* @param scoreDirector the {@link ScoreDirector} that needs to get notified of the changes.
* @param isPhaseTerminated long-running command implementations should check this periodically
* and terminate early if it returns true.
* Otherwise the terminations configured by the user will have no effect,
* as the solver can only terminate itself when a command has ended.
* @param context the context of the command, providing access to the working solution and allowing move execution
*/
void changeWorkingSolution(ScoreDirector<Solution_> scoreDirector, BooleanSupplier isPhaseTerminated);
void changeWorkingSolution(PhaseCommandContext<Solution_> context);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ai.timefold.solver.core.api.solver.phase;

import java.util.function.Function;

import ai.timefold.solver.core.api.domain.common.Lookup;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
import ai.timefold.solver.core.preview.api.move.Move;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* The context of a command that is executed during a custom phase.
* It provides access to the working solution and allows executing moves.
*
* @param <Solution_> the type of the solution
* @see PhaseCommand
*/
@NullMarked
public interface PhaseCommandContext<Solution_>
extends Lookup {

/**
* Returns the meta-model of the {@link #getWorkingSolution() working solution}.
*
* @return the meta-model of the working solution
*/
PlanningSolutionMetaModel<Solution_> getSolutionMetaModel();

/**
* Returns the current working solution.
* It must not be modified directly,
* but only through {@link #execute(Move)} or {@link #executeTemporarily(Move, Function)}.
* Direct modifications will cause the solver to be in an inconsistent state and likely throw an exception later on.
*
* @return the current working solution
*/
Solution_ getWorkingSolution();

/**
* Long-running command implementations should check this periodically and terminate early if it returns true.
* Otherwise the terminations configured by the user will have no effect,
* as the solver can only terminate itself when a command has ended.
*
* @return true if the solver has requested the phase to terminate,
* for example because the time limit has been reached.
*/
boolean isPhaseTerminated();

/**
* As defined by {@link #execute(Move, boolean)},
* but with the guarantee of a fresh score.
*/
default void execute(Move<Solution_> move) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the execute commands should return the score (that way, users don't need to go through getWorkingSolution).

execute(move, true);
}

/**
* Executes the given move and updates the working solution,
* optionally without recalculating the score for performance reasons.
*
* @param move the move to execute
* @param guaranteeFreshScore if true, the score of {@link #getWorkingSolution()} after this method returns
* is guaranteed to be up-to-date;
* otherwise it may be stale as the solver will skip recalculating it for performance reasons.
*/
void execute(Move<Solution_> move, boolean guaranteeFreshScore);

/**
* As defined by {@link #executeTemporarily(Move, Function, boolean)},
* with the guarantee of a fresh score.
*/
default <Result_> @Nullable Result_ executeTemporarily(Move<Solution_> move,
Function<Solution_, @Nullable Result_> temporarySolutionConsumer) {
return executeTemporarily(move, temporarySolutionConsumer, true);
}

/**
* Executes the given move temporarily and returns the result of the given consumer.
* The working solution is reverted to its original state after the consumer has been executed,
* optionally without recalculating the score for performance reasons.
*
* @param move the move to execute temporarily
* @param temporarySolutionConsumer the consumer to execute with the temporarily modified solution;
* this solution must not be modified any further.
* @param guaranteeFreshScore if true, the score of {@link #getWorkingSolution()} after this method returns
* is guaranteed to be up-to-date;
* otherwise it may be stale as the solver will skip recalculating it for performance reasons.
* @return the result of the consumer
*/
<Result_> @Nullable Result_ executeTemporarily(Move<Solution_> move,
Function<Solution_, @Nullable Result_> temporarySolutionConsumer, boolean guaranteeFreshScore);

@Override
<T> @Nullable T lookUpWorkingObject(@Nullable T problemFactOrPlanningEntity);

@Override
<T> @Nullable T lookUpWorkingObjectOrFail(@Nullable T problemFactOrPlanningEntity);

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import jakarta.xml.bind.annotation.XmlType;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import ai.timefold.solver.core.api.score.director.ScoreDirector;
import ai.timefold.solver.core.api.solver.Solver;
import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig;
import ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchPhaseConfig;
Expand Down Expand Up @@ -81,7 +80,7 @@ public void setSolutionPartitionerCustomProperties(@Nullable Map<String, String>
*
* <p>
* The number of {@link Thread}s is always equal to the number of partitions returned by
* {@link SolutionPartitioner#splitWorkingSolution(ScoreDirector, Integer)},
* {@link SolutionPartitioner#splitWorkingSolution(Object, Integer)},
* because otherwise some partitions would never run (especially with {@link Solver#terminateEarly() asynchronous
* termination}).
* If this limit (or {@link Runtime#availableProcessors()}) is lower than the number of partitions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package ai.timefold.solver.core.impl.domain.lookup;
package ai.timefold.solver.core.impl.domain.common;

import java.util.Map;

import org.jspecify.annotations.NullMarked;

@NullMarked
final class ImmutableLookUpStrategy implements LookUpStrategy {
final class ImmutableLookupStrategy implements LookupStrategy {

@Override
public void addWorkingObject(Map<Object, Object> idToWorkingObjectMap, Object workingObject) {
Expand Down
Loading
Loading