Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
27 changes: 27 additions & 0 deletions core/src/build/revapi-differences.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,33 @@
"new": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig",
"annotation": "@org.jspecify.annotations.NullMarked",
"justification": "Update config"
},
{
"ignore": true,
"code": "java.field.removed",
"old": "field java.util.EventObject.source @ ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
},
{
"ignore": true,
"code": "java.method.removed",
"old": "method java.lang.Object java.util.EventObject::getSource() @ ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
},
{
"ignore": true,
"code": "java.class.noLongerInheritsFromClass",
"old": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
"new": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
},
{
"ignore": true,
"code": "java.class.noLongerImplementsInterface",
"old": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
"new": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
"interface": "java.io.Serializable",
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import java.util.function.Function;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.solver.event.FinalBestSolutionEvent;
import ai.timefold.solver.core.api.solver.event.FirstInitializedSolutionEvent;
import ai.timefold.solver.core.api.solver.event.NewBestSolutionEvent;
import ai.timefold.solver.core.api.solver.event.SolverJobStartedEvent;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
Expand Down Expand Up @@ -57,41 +61,83 @@ public interface SolverJobBuilder<Solution_, ProblemId_> {
SolverJobBuilder<Solution_, ProblemId_>
withProblemFinder(@NonNull Function<? super ProblemId_, ? extends Solution_> problemFinder);

/**
* As defined by {@link #withBestSolutionEventConsumer(Consumer)}.
*
* @deprecated Use {@link #withBestSolutionEventConsumer(Consumer)} instead.
*/
@Deprecated(forRemoval = true, since = "1.28.0")
@NonNull
default SolverJobBuilder<Solution_, ProblemId_>
withBestSolutionConsumer(@NonNull Consumer<? super Solution_> bestSolutionConsumer) {
return withBestSolutionEventConsumer(event -> bestSolutionConsumer.accept(event.solution()));
}

/**
* Sets the best solution consumer, which may be called multiple times during the solving process.
* <p>
* Don't apply any changes to the solution instance while the solver runs.
* The solver's best solution instance is the same as the one in the event,
* and any modifications may lead to solver corruption due to its internal reuse.
*
* @param bestSolutionConsumer called multiple times for each new best solution on a consumer thread
* @param bestSolutionEventConsumer called multiple times for each new best solution on a consumer thread
* @return this
*/
@NonNull
SolverJobBuilder<Solution_, ProblemId_> withBestSolutionConsumer(@NonNull Consumer<? super Solution_> bestSolutionConsumer);
SolverJobBuilder<Solution_, ProblemId_>
withBestSolutionEventConsumer(@NonNull Consumer<NewBestSolutionEvent<Solution_>> bestSolutionEventConsumer);

/**
* As defined by {@link #withFinalBestSolutionEventConsumer}.
*
* @deprecated Use {@link #withFinalBestSolutionEventConsumer(Consumer)} instead.
*/
@Deprecated(forRemoval = true, since = "1.28.0")
@NonNull
default SolverJobBuilder<Solution_, ProblemId_>
withFinalBestSolutionConsumer(@NonNull Consumer<? super Solution_> finalBestSolutionConsumer) {
return withFinalBestSolutionEventConsumer(event -> finalBestSolutionConsumer.accept(event.solution()));
}

/**
* Sets the final best solution consumer, which is called at the end of the solving process and returns the final
* best solution.
*
* @param finalBestSolutionConsumer called only once at the end of the solving process on a consumer thread
* @param finalBestSolutionEventConsumer called only once at the end of the solving process on a consumer thread
* @return this
*/
@NonNull
SolverJobBuilder<Solution_, ProblemId_>
withFinalBestSolutionConsumer(@NonNull Consumer<? super Solution_> finalBestSolutionConsumer);
withFinalBestSolutionEventConsumer(
@NonNull Consumer<FinalBestSolutionEvent<Solution_>> finalBestSolutionEventConsumer);

/**
* As defined by #withFirstInitializedSolutionConsumer(FirstInitializedSolutionConsumer).
* As defined by {@link #withFirstInitializedSolutionEventConsumer(Consumer)}.
*
* @deprecated Use {@link #withFirstInitializedSolutionConsumer(FirstInitializedSolutionConsumer)} instead.
* @deprecated Use {@link #withFirstInitializedSolutionEventConsumer(Consumer)} 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));
return withFirstInitializedSolutionEventConsumer(
(event) -> firstInitializedSolutionConsumer.accept(event.solution()));
}

/**
* As defined by {@link #withFirstInitializedSolutionEventConsumer(Consumer)}.
*
* @deprecated Use {@link #withFirstInitializedSolutionEventConsumer(Consumer)} instead.
*
* @param firstInitializedSolutionConsumer called only once before starting the first Local Search phase
* @return this
*/
@Deprecated(forRemoval = true, since = "1.28.0")
@NonNull
default SolverJobBuilder<Solution_, ProblemId_> withFirstInitializedSolutionConsumer(
@NonNull FirstInitializedSolutionConsumer<? super Solution_> firstInitializedSolutionConsumer) {
return withFirstInitializedSolutionEventConsumer(
event -> firstInitializedSolutionConsumer.accept(event.solution(), event.isTerminatedEarly()));
}

/**
Expand All @@ -100,20 +146,30 @@ public interface SolverJobBuilder<Solution_, ProblemId_> {
* 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
* @param firstInitializedSolutionEventConsumer called only once before starting the first Local Search phase
* @return this
*/
@NonNull
SolverJobBuilder<Solution_, ProblemId_> withFirstInitializedSolutionConsumer(
@NonNull FirstInitializedSolutionConsumer<? super Solution_> firstInitializedSolutionConsumer);
SolverJobBuilder<Solution_, ProblemId_> withFirstInitializedSolutionEventConsumer(
@NonNull Consumer<FirstInitializedSolutionEvent<Solution_>> firstInitializedSolutionEventConsumer);

/**
* As defined by {@link #withSolverJobStartedEventConsumer(Consumer)}.
*
* @deprecated Use {@link #withSolverJobStartedEventConsumer(Consumer)} instead.
*/
default SolverJobBuilder<Solution_, ProblemId_>
withSolverJobStartedConsumer(Consumer<? super Solution_> solverJobStartedConsumer) {
return withSolverJobStartedEventConsumer(event -> solverJobStartedConsumer.accept(event.solution()));
}

/**
* Sets the consumer for when the solver starts its solving process.
*
* @param solverJobStartedConsumer never null, called only once when the solver is starting the solving process
* @return this, never null
*/
SolverJobBuilder<Solution_, ProblemId_> withSolverJobStartedConsumer(Consumer<? super Solution_> solverJobStartedConsumer);
SolverJobBuilder<Solution_, ProblemId_>
withSolverJobStartedEventConsumer(Consumer<SolverJobStartedEvent<Solution_>> solverJobStartedConsumer);

/**
* Sets the custom exception handler.
Expand Down Expand Up @@ -166,5 +222,4 @@ interface FirstInitializedSolutionConsumer<Solution_> {
void accept(Solution_ solution, boolean isTerminatedEarly);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public interface SolverManager<Solution_, ProblemId_> extends AutoCloseable {
.withProblemId(problemId)
.withProblem(problem);
if (finalBestSolutionConsumer != null) {
builder.withFinalBestSolutionConsumer(finalBestSolutionConsumer);
builder.withFinalBestSolutionEventConsumer(event -> finalBestSolutionConsumer.accept(event.solution()));
}
return builder.run();
}
Expand Down Expand Up @@ -297,7 +297,7 @@ public interface SolverManager<Solution_, ProblemId_> extends AutoCloseable {
return solveBuilder()
.withProblemId(problemId)
.withProblem(problem)
.withBestSolutionConsumer(bestSolutionConsumer)
.withBestSolutionEventConsumer(event -> bestSolutionConsumer.accept(event.solution()))
.run();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package ai.timefold.solver.core.api.solver.event;

import java.util.EventObject;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.solver.Solver;
Expand All @@ -16,9 +14,9 @@
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
// TODO In Solver 2.0, maybe convert this to an interface.
public class BestSolutionChangedEvent<Solution_> extends EventObject {

public class BestSolutionChangedEvent<Solution_> {
private final Solver<Solution_> solver;
private final EventProducerId producerId;
private final long timeMillisSpent;
private final Solution_ newBestSolution;
private final Score newBestScore;
Expand All @@ -31,7 +29,7 @@ public class BestSolutionChangedEvent<Solution_> extends EventObject {
@Deprecated(forRemoval = true, since = "1.22.0")
public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, long timeMillisSpent,
@NonNull Solution_ newBestSolution, @NonNull Score newBestScore) {
this(solver, timeMillisSpent, newBestSolution, newBestScore, true);
this(solver, EventProducerId.unknown(), timeMillisSpent, newBestSolution, newBestScore, true);
}

/**
Expand All @@ -42,8 +40,19 @@ public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, long timeMill
public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, long timeMillisSpent,
@NonNull Solution_ newBestSolution, @NonNull Score newBestScore,
boolean isNewBestSolutionInitialized) {
super(solver);
this(solver, EventProducerId.unknown(), timeMillisSpent, newBestSolution, newBestScore, isNewBestSolutionInitialized);
}

/**
* @param timeMillisSpent {@code >= 0L}
* @deprecated Users should not manually construct instances of this event.
*/
@Deprecated(forRemoval = true, since = "1.28.0")
public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, EventProducerId producerId, long timeMillisSpent,
@NonNull Solution_ newBestSolution, @NonNull Score newBestScore,
boolean isNewBestSolutionInitialized) {
this.solver = solver;
this.producerId = producerId;
this.timeMillisSpent = timeMillisSpent;
this.newBestSolution = newBestSolution;
this.newBestScore = newBestScore;
Expand All @@ -58,6 +67,13 @@ public long getTimeMillisSpent() {
return timeMillisSpent;
}

/**
* @return A {@link EventProducerId} identifying what generated the event
*/
public EventProducerId getProducerId() {
return producerId;
}

/**
* Note that:
* <ul>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package ai.timefold.solver.core.api.solver.event;

import java.util.OptionalInt;

import ai.timefold.solver.core.api.solver.change.ProblemChange;
import ai.timefold.solver.core.config.solver.SolverConfig;
import ai.timefold.solver.core.impl.phase.NoChangePhase;
import ai.timefold.solver.core.impl.phase.PhaseType;
import ai.timefold.solver.core.impl.phase.event.PhaseEventProducerId;
import ai.timefold.solver.core.impl.solver.event.SolveEventProducerId;

import org.jspecify.annotations.NullMarked;

/**
* Identifies the producer of a {@link BestSolutionChangedEvent}.
*/
@NullMarked
public interface EventProducerId {
/**
* An unique string identifying what produced the event, either of the form
* "Event" where "Event" is a string describing the event that cause the update (like "Solving started")
* or "Phase (index)", where "Phase" is a string identifying the type of phase (like "Construction Heuristics")
* and index is the index of the phase in the {@link SolverConfig#getPhaseConfigList()}.
*
* @return An unique string identifying what produced the event.
*/
String producerId();

/**
* A (non-unique) string describing what produced the event.
* Events from different phases of the same type (for example,
* when multiple Construction Heuristics are configured)
* will return the same value.
*
* @return A (non-unique) string describing what produced the event.
*/
String simpleProducerName();

/**
* If present, the index of the phase that produced the event in the {@link SolverConfig#getPhaseConfigList()}.
* Is absent when the producer does not correspond to a phase, for instance,
* an event triggered after {@link ProblemChange} were processed.
*
* @return The index of the corresponding phase in {@link SolverConfig#getPhaseConfigList()},
* or {@link OptionalInt#empty()} if there is no corresponding phase.
*/
OptionalInt phaseIndex();

static EventProducerId unknown() {
return SolveEventProducerId.UNKNOWN;
}

static EventProducerId solvingStarted() {
return SolveEventProducerId.SOLVING_STARTED;
}

static EventProducerId problemChange() {
return SolveEventProducerId.PROBLEM_CHANGE;
}

/**
* @deprecated Deprecated on account of {@link NoChangePhase} having no use.
*/
@Deprecated(forRemoval = true, since = "1.28.0")
static EventProducerId noChange(int phaseIndex) {
return new PhaseEventProducerId(PhaseType.NO_CHANGE, phaseIndex);
}

static EventProducerId constructionHeuristic(int phaseIndex) {
return new PhaseEventProducerId(PhaseType.CONSTRUCTION_HEURISTIC, phaseIndex);
}

static EventProducerId localSearch(int phaseIndex) {
return new PhaseEventProducerId(PhaseType.LOCAL_SEARCH, phaseIndex);
}

static EventProducerId exhaustiveSearch(int phaseIndex) {
return new PhaseEventProducerId(PhaseType.EXHAUSTIVE_SEARCH, phaseIndex);
}

static EventProducerId partitionedSearch(int phaseIndex) {
return new PhaseEventProducerId(PhaseType.PARTITIONED_SEARCH, phaseIndex);
}

static EventProducerId customPhase(int phaseIndex) {
return new PhaseEventProducerId(PhaseType.CUSTOM_PHASE, phaseIndex);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ai.timefold.solver.core.api.solver.event;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;

/**
* Delivered in a consumer thread at the end of the solving process and contains the final {@link PlanningSolution best
* solution} found.
*
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
public interface FinalBestSolutionEvent<Solution_> {
/**
* @return the {@link PlanningSolution best solution} found by the solver
*/
Solution_ solution();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ai.timefold.solver.core.api.solver.event;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;

/**
* Delivered in a consumer thread at 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 <Solution_>
*/
public interface FirstInitializedSolutionEvent<Solution_> {
/**
* @return The {@link PlanningSolution initialized solution}
*/
Solution_ solution();

/**
* @return A {@link EventProducerId} identifying what generated the event
*/
EventProducerId producerId();

/**
* @return True if the solver was terminated early, false otherwise
*/
boolean isTerminatedEarly();
}
Loading
Loading