Skip to content

Commit

Permalink
feat: support assigned values for Recommendations as well (#1136)
Browse files Browse the repository at this point in the history
  • Loading branch information
triceo authored Oct 7, 2024
1 parent cf473d7 commit 11cd2fb
Show file tree
Hide file tree
Showing 22 changed files with 1,575 additions and 261 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package ai.timefold.solver.core.api.solver;

import java.util.function.Function;

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis;
import ai.timefold.solver.core.api.score.analysis.MatchAnalysis;
import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;

/**
* Represents the result of the Assignment Recommendation API,
* see {@link SolutionManager#recommendFit(Object, Object, Function)}.
*
* <p>
* In order to be fully serializable to JSON, propositions must be fully serializable to JSON.
* For deserialization from JSON, the user needs to provide the deserializer themselves.
* This is due to the fact that, once the proposition is received over the wire,
* we no longer know which type was used.
* The user has all of that information in their domain model,
* and so they are the correct party to provide the deserializer.
* See also {@link ScoreAnalysis} Javadoc for additional notes on serializing and deserializing that type.
*
* @param <Proposition_> the generic type of the proposition as returned by the proposition function
* @param <Score_> the generic type of the score
*/
public interface RecommendedAssignment<Proposition_, Score_ extends Score<Score_>> {

/**
* Returns the proposition as returned by the proposition function.
* This is the actual assignment recommended to the user.
*
* @return null if proposition function required null
*/
Proposition_ proposition();

/**
* Difference between the original score and the score of the solution with the recommendation applied.
*
* <p>
* If {@link SolutionManager#recommendAssignment(Object, Object, Function, ScoreAnalysisFetchPolicy)} was called with
* {@link ScoreAnalysisFetchPolicy#FETCH_ALL},
* the analysis will include {@link MatchAnalysis constraint matches}
* inside its {@link ConstraintAnalysis constraint analysis};
* otherwise it will not.
*
* @return never null; {@code fittedScoreAnalysis - originalScoreAnalysis} as defined by
* {@link ScoreAnalysis#diff(ScoreAnalysis)}
*/
ScoreAnalysis<Score_> scoreAnalysisDiff();

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import java.util.function.Function;

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis;
import ai.timefold.solver.core.api.score.analysis.MatchAnalysis;
import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;

/**
Expand All @@ -22,30 +20,10 @@
*
* @param <Proposition_> the generic type of the proposition as returned by the proposition function
* @param <Score_> the generic type of the score
* @deprecated Prefer {@link RecommendedAssignment} instead.
*/
public interface RecommendedFit<Proposition_, Score_ extends Score<Score_>> {

/**
* Returns the proposition as returned by the proposition function.
* This is the actual assignment recommended to the user.
*
* @return null if proposition function required null
*/
Proposition_ proposition();

/**
* Difference between the original score and the score of the solution with the recommendation applied.
*
* <p>
* If {@link SolutionManager#recommendFit(Object, Object, Function, ScoreAnalysisFetchPolicy)} was called with
* {@link ScoreAnalysisFetchPolicy#FETCH_ALL},
* the analysis will include {@link MatchAnalysis constraint matches}
* inside its {@link ConstraintAnalysis constraint analysis};
* otherwise it will not.
*
* @return never null; {@code fittedScoreAnalysis - originalScoreAnalysis} as defined by
* {@link ScoreAnalysis#diff(ScoreAnalysis)}
*/
ScoreAnalysis<Score_> scoreAnalysisDiff();
@Deprecated(forRemoval = true, since = "1.15.0")
public interface RecommendedFit<Proposition_, Score_ extends Score<Score_>>
extends RecommendedAssignment<Proposition_, Score_> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -138,34 +138,32 @@ ScoreAnalysis<Score_> analyze(Solution_ solution, ScoreAnalysisFetchPolicy fetch
SolutionUpdatePolicy solutionUpdatePolicy);

/**
* As defined by {@link #recommendFit(Object, Object, Function, ScoreAnalysisFetchPolicy)},
* As defined by {@link #recommendAssignment(Object, Object, Function, ScoreAnalysisFetchPolicy)},
* with {@link ScoreAnalysisFetchPolicy#FETCH_ALL}.
*/
default <EntityOrElement_, Proposition_> List<RecommendedFit<Proposition_, Score_>> recommendFit(Solution_ solution,
EntityOrElement_ fittedEntityOrElement, Function<EntityOrElement_, Proposition_> propositionFunction) {
return recommendFit(solution, fittedEntityOrElement, propositionFunction, FETCH_ALL);
default <EntityOrElement_, Proposition_> List<RecommendedAssignment<Proposition_, Score_>> recommendAssignment(
Solution_ solution, EntityOrElement_ evaluatedEntityOrElement,
Function<EntityOrElement_, Proposition_> propositionFunction) {
return recommendAssignment(solution, evaluatedEntityOrElement, propositionFunction, FETCH_ALL);
}

/**
* Quickly runs through all possible options of fitting a given entity or element in a given solution,
* Quickly runs through all possible options of assigning a given entity or element in a given solution,
* and returns a list of recommendations sorted by score,
* with most favorable score first.
* Think of this method as a construction heuristic
* which shows you all the options to initialize the solution.
* The input solution must be fully initialized
* except for one entity or element, the one to be fitted.
* The input solution must either be fully initialized,
* or have a single entity or element unassigned.
*
* <p>
* For problems with only basic planning variables or with chained planning variables,
* the fitted element is a planning entity of the problem.
* Each available planning value will be tested for fit
* by setting it to the planning variable in question.
* Each available planning value will be tested by setting it to the planning variable in question.
* For problems with a list variable,
* the fitted element may be a shadow entity,
* and it will be tested for fit in each position of the planning list variable.
* the evaluated element may be a shadow entity,
* and it will be tested in each position of the planning list variable.
*
* <p>
* The score returned by {@link RecommendedFit#scoreAnalysisDiff()}
* The score returned by {@link RecommendedAssignment#scoreAnalysisDiff()}
* is the difference between the score of the solution before and after fitting.
* Every recommendation will be in a state as if the solution was never changed;
* if it references entities,
Expand All @@ -182,7 +180,7 @@ default <EntityOrElement_, Proposition_> List<RecommendedFit<Proposition_, Score
* or indirectly by not providing correct data.
*
* <p>
* When an element is tested for fit,
* When an element is tested,
* a score is calculated over the entire solution with the element in place,
* also called a placement.
* The proposition function is also called at that time,
Expand Down Expand Up @@ -217,7 +215,7 @@ default <EntityOrElement_, Proposition_> List<RecommendedFit<Proposition_, Score
* and therefore share the same state.
* </li>
* <li>The placement is then cleared again,
* both elements have been tested for fit,
* both elements have been tested,
* and solution is returned to its original order.
* The propositions are then returned to the user,
* who notices that both P1 and P2 are {@code mondayShift@null}.
Expand All @@ -230,22 +228,46 @@ default <EntityOrElement_, Proposition_> List<RecommendedFit<Proposition_, Score
* this problem would have been avoided.
* Alternatively, the proposition function could have returned a defensive copy of the Shift.
*
* @param solution never null; must be fully initialized except for one entity or element
* @param fittedEntityOrElement never null; must be part of the solution
* @param solution never null; for basic variable, must be fully initialized or have a single entity unassigned.
* For list variable, all values must be assigned to some list, with the optional exception of one.
* @param evaluatedEntityOrElement never null; must be part of the solution.
* For basic variable, it is a planning entity and may have one or more variables unassigned.
* For list variable, it is a shadow entity and need not be present in any list variable.
* @param propositionFunction never null
* @param fetchPolicy never null;
* {@link ScoreAnalysisFetchPolicy#FETCH_ALL} will include more data within {@link RecommendedFit},
* {@link ScoreAnalysisFetchPolicy#FETCH_ALL} will include more data within {@link RecommendedAssignment},
* but will also take more time to gather that data.
* @return never null, sorted from best to worst;
* designed to be JSON-friendly, see {@link RecommendedFit} Javadoc for more.
* @param <EntityOrElement_> generic type of the unassigned entity or element
* designed to be JSON-friendly, see {@link RecommendedAssignment} Javadoc for more.
* @param <EntityOrElement_> generic type of the evaluated entity or element
* @param <Proposition_> generic type of the user-provided proposition;
* if it is a planning entity, it is recommended
* to make a defensive copy inside the proposition function.
* @see PlanningEntity More information about genuine and shadow planning entities.
*/
<EntityOrElement_, Proposition_> List<RecommendedAssignment<Proposition_, Score_>> recommendAssignment(Solution_ solution,
EntityOrElement_ evaluatedEntityOrElement, Function<EntityOrElement_, Proposition_> propositionFunction,
ScoreAnalysisFetchPolicy fetchPolicy);

/**
* As defined by {@link #recommendAssignment(Object, Object, Function, ScoreAnalysisFetchPolicy)},
* with {@link ScoreAnalysisFetchPolicy#FETCH_ALL}.
*
* @deprecated Prefer {@link #recommendAssignment(Object, Object, Function, ScoreAnalysisFetchPolicy)}.
*/
@Deprecated(forRemoval = true, since = "1.15.0")
default <EntityOrElement_, Proposition_> List<RecommendedFit<Proposition_, Score_>> recommendFit(Solution_ solution,
EntityOrElement_ fittedEntityOrElement, Function<EntityOrElement_, Proposition_> propositionFunction) {
return recommendFit(solution, fittedEntityOrElement, propositionFunction, FETCH_ALL);
}

/**
* As defined by {@link #recommendAssignment(Object, Object, Function, ScoreAnalysisFetchPolicy)}.
*
* @deprecated Prefer {@link #recommendAssignment(Object, Object, Function, ScoreAnalysisFetchPolicy)}.
*/
@Deprecated(forRemoval = true, since = "1.15.0")
<EntityOrElement_, Proposition_> List<RecommendedFit<Proposition_, Score_>> recommendFit(Solution_ solution,
EntityOrElement_ fittedEntityOrElement, Function<EntityOrElement_, Proposition_> propositionFunction,
ScoreAnalysisFetchPolicy fetchPolicy);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,47 @@
import java.util.function.Function;

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.solver.RecommendedFit;
import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy;
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;

final class Fitter<Solution_, In_, Out_, Score_ extends Score<Score_>>
implements Function<InnerScoreDirector<Solution_, Score_>, List<RecommendedFit<Out_, Score_>>> {
final class Assigner<Solution_, Score_ extends Score<Score_>, Recommendation_, In_, Out_>
implements Function<InnerScoreDirector<Solution_, Score_>, List<Recommendation_>> {

private final DefaultSolverFactory<Solution_> solverFactory;
private final Solution_ originalSolution;
private final In_ originalElement;
private final Function<In_, Out_> propositionFunction;
private final RecommendationConstructor<Score_, Recommendation_, Out_> recommendationConstructor;
private final ScoreAnalysisFetchPolicy fetchPolicy;
private final Solution_ originalSolution;
private final In_ originalElement;

public Fitter(DefaultSolverFactory<Solution_> solverFactory, Solution_ originalSolution, In_ originalElement,
Function<In_, Out_> propositionFunction, ScoreAnalysisFetchPolicy fetchPolicy) {
public Assigner(DefaultSolverFactory<Solution_> solverFactory, Function<In_, Out_> propositionFunction,
RecommendationConstructor<Score_, Recommendation_, Out_> recommendationConstructor,
ScoreAnalysisFetchPolicy fetchPolicy, Solution_ originalSolution, In_ originalElement) {
this.solverFactory = Objects.requireNonNull(solverFactory);
this.originalSolution = Objects.requireNonNull(originalSolution);
this.originalElement = Objects.requireNonNull(originalElement);
this.propositionFunction = Objects.requireNonNull(propositionFunction);
this.recommendationConstructor = Objects.requireNonNull(recommendationConstructor);
this.fetchPolicy = Objects.requireNonNull(fetchPolicy);
this.originalSolution = Objects.requireNonNull(originalSolution);
this.originalElement = Objects.requireNonNull(originalElement);
}

@Override
public List<RecommendedFit<Out_, Score_>> apply(InnerScoreDirector<Solution_, Score_> scoreDirector) {
public List<Recommendation_> apply(InnerScoreDirector<Solution_, Score_> scoreDirector) {
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var initializationStatistics = solutionDescriptor.computeInitializationStatistics(originalSolution);
var uninitializedCount =
initializationStatistics.uninitializedEntityCount() + initializationStatistics.unassignedValueCount();
if (uninitializedCount > 1) {
throw new IllegalStateException("""
Solution (%s) has (%d) uninitialized elements.
Fit Recommendation API requires at most one uninitialized element in the solution."""
Assignment Recommendation API requires at most one uninitialized element in the solution."""
.formatted(originalSolution, uninitializedCount));
}
var originalScoreAnalysis = scoreDirector.buildScoreAnalysis(fetchPolicy == ScoreAnalysisFetchPolicy.FETCH_ALL,
InnerScoreDirector.ScoreAnalysisMode.RECOMMENDATION_API);
var clonedElement = scoreDirector.lookUpWorkingObject(originalElement);
var processor =
new FitProcessor<>(solverFactory, propositionFunction, originalScoreAnalysis, clonedElement, fetchPolicy);
var processor = new AssignmentProcessor<>(solverFactory, propositionFunction, recommendationConstructor, fetchPolicy,
clonedElement, originalScoreAnalysis);
return processor.apply(scoreDirector);
}

Expand Down
Loading

0 comments on commit 11cd2fb

Please sign in to comment.