Reputation: 1073
I have a @PlanningEntity
called a Participation
, where the @PlanningVariable
enrolled
indicates whether Student
is attending the Appointment
in question:
@PlanningEntity
public class Participation {
private Student student;
private Appointment appointment;
@PlanningVariable
private Boolean enrolled = false;
}
In my specific use-case, the best solution is usually one where most (but not all) of the Participation
are attended. In fact, if I initialize my PlanningVariable
to null
instead of false
and start with a FIRST_FIT
construction phase, I get a performance improvement of around 3-4x to find the same solution (or the same score at least). I imagine because most enrolled
values are set to true
more efficiently than is possible during a LocalSearchPhase
.
The data-model backing my PlanningSolution
currently does not differentiate between a Participation
not being attended (enrolled = false
) and a choice not yet having been made (enrolled = null
). So even though this is a possible solution, ideally I would like to prevent changing the underlying data by initializing my PlanningEntity
s with a fake enrolled
value (setting it to null
preemptively).
I have tried experimenting with a custom Forager config, fox example by using .withPickEarlyType(LocalSearchPickEarlyType.FIRST_BEST_SCORE_IMPROVING)
and implementing a custom SelectionFilter
that only selects entities with enrolled==false
, but this does not seem to make a lot of difference.
So, the question: is it possible to do a FIRST_FIT
-like construction on a solution where the current solution is already initialized? I would basically like to tell Timefold to look at each entity exactly once, and greedily set enrolled=true
if this results in a better solution score.
EDIT: Another attempt was implementing a CustomPhaseCommand
as the first phase of my solver. This intuitively seems as a logical way to go, however I don't know how to check the impact of a change on the score of my solution. If I implement changeWorkingSolution
to set all enrolled
values to true
, the Phase is not executed since setting every value to true does not improve the solution score. Is there a way to check for an improvement from within a CustomPhaseCommand
?
EDIT2: See my 'answer' below
Upvotes: 0
Views: 500
Reputation: 5702
I am not entirely sure why you need the solver to do this. To quote you:
So, the question: is it possible to do a FIRST_FIT-like construction on a solution where the current solution is already initialized? I would basically like to tell Timefold to look at each entity exactly once, and greedily set enrolled=true if this results in a better solution score.
This is not really a problem for the local search, or for a constraint solver. This is a simple loop that modifies the solution and calculates the score at each step. I suggest you check out SolutionManager#update(Solution)
, and use that in your loop.
You will lose all the performance benefits of incremental calculation. If it's necessary to do this very fast, doing the same thing the SolutionManager
does through InnerScoreDirector
is possible. Be advised though that InnerScoreDirector
and associated APIs are private and are not guaranteed to be stable.
Upvotes: 0
Reputation: 1073
I have found a solution that works exactly as expected through the CustomPhaseCommand
. I have implemented the command as follows:
class InitialConstructionPhase implements CustomPhaseCommand<Timetable> {
@Override
public void changeWorkingSolution(ScoreDirector<Timetable> scoreDirector) {
InnerScoreDirector<Timetable, HardSoftScore> innerScoreDirector = (InnerScoreDirector<Timetable, HardSoftScore>) scoreDirector;
scoreDirector.getWorkingSolution().getParticipations().stream().filter(participation -> {
return !participation.getEnrolled();
}).forEach(participation -> {
HardSoftScore oldScore = innerScoreDirector.calculateScore();
setParticipationEnrollment(scoreDirector, participation, true);
HardSoftScore newScore = innerScoreDirector.calculateScore();
if(oldScore.compareTo(newScore) > 0) {
setParticipationEnrollment(scoreDirector, participation, false);
}
});
}
private void setParticipationEnrollment(ScoreDirector<Timetable> scoreDirector, Participation participation, boolean enrolled) {
scoreDirector.beforeVariableChanged(participation, "enrolled");
participation.setEnrolled(enrolled);
scoreDirector.afterVariableChanged(participation, "enrolled");
scoreDirector.triggerVariableListeners();
}
}
I found the case to InnerScoreDirector
in https://github.com/TimefoldAI/timefold-solver/blob/7bb9cc2b67c51e88f1ef72bfa940b5df0de9bb91/core/core-impl/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java#L276C13-L277C77 . Is this the recommended way to achieve this? Doing a cast to InnerScoreDirector
like this feels weird, but I don't see another way to calculate the problem score during the CustomPhaseCommand.
Notably, I see that in Optaplanner 7.29.0.Final, a calculateScore
method was available on a ScoreDirector
, but I guess this was removed?
Upvotes: 0