'How to penalize gaps between days in OptaPlanner constraint stream?
I have a model where each Course
has a list of available TimeSlot
s from which one TimeSlot
gets selected by OptaPlanner. Each TimeSlot
has a dayOfWeek
property. The weeks are numbered from 1 starting with Monday.
Let's say the TimeSlot
s are allocated such that they occupy days 1, 3, and 5. This should be penalized by 2 since there's one free day between Monday and Wednesday, and one free day between Wednesday and Friday. By using groupBy(course -> course.getSelectedTimeslot().getDayOfWeek().getValue())
, we can get a list of occupied days.
One idea is to use a collector like sum()
, for example, and write something like sum((day1, day2) -> day2 - day1 - 1)
, but sum()
, of course, works with only one argument. But generally, maybe this could be done by using a custom constraint collector, however, I do not know whether these collectors can perform such a specific action.
Another idea is that instead of summing up the differences directly, we could simply map each consecutive pair of days (assuming they're ordered) to the difference with the upcoming one. Penalization with the weight of value would then perform the summing for us. For example, 1, 4, 5
would map onto 2, 0
, and we could then penalize for each item with the weight of its value.
If I had the weeks in an array, the code would look like this:
public static int penalize(int[] weeks) {
Arrays.sort(weeks);
int sumOfDifferences = 0;
for (int i = 1; i < weeks.length; i++) {
sumOfDifferences += weeks[i] - weeks[i - 1] - 1;
}
return sumOfDifferences;
}
How can we perform penalization of gaps between days using constraint collectors?
Solution 1:[1]
An approach using a constraint collector is certainly possible, see ExperimentalCollectors
in optaplanner-examples
module, and its use in the Nurse Rostering example.
However, for this case, I think that would be an overkill. Instead, think about "two days with a gap inbetween" as "two days at least 1 day apart, with no day inbetween". Once you reformulate your problem like that, ifNotExists(...)
is your friend.
forEachUniquePair(Timeslot.class,
Joiner.greaterThan(slot -> slot.dayOfWeek + 1))
.ifNotExists(Timeslot.class,
Joiners.lessThan((slot1, slot2) -> slot1.dayOfWeek, TimeSlot::dayOfWeek),
Joiners.greaterThan((slot1, slot2) -> slot2.dayOfWeek, TimeSlot::dayOfWeek))
...
Obviously this is just pseudo-code, you will have to adapt it to your particular situation, but it should give you an idea for how to approach the problem.
Solution 2:[2]
I resorted to writing my own constraint collector
public static <T> UniConstraintCollector<T, Set<Integer>, Integer> gapsCollector(ToIntFunction<T> toIntFunction) {
return new DefaultUniConstraintCollector<>(HashSet::new, (acc, a) -> {
acc.add(toIntFunction.applyAsInt(a));
return () -> acc.remove(toIntFunction.applyAsInt(a));
}, (durationWrapper) -> {
List<Integer> timesArray = new ArrayList<>(durationWrapper);
Collections.sort(timesArray);
int sumOfDifferences = 0;
for (int i = 1; i < timesArray.size(); i++) {
sumOfDifferences += timesArray.get(i) - timesArray.get(i - 1) - 1;
}
return sumOfDifferences;
});
}
and used it on all the courses:
constraintFactory.forEach(Event.class)
.groupBy(gapsCollector(e -> e.getSelectedTimeslot().getDayOfWeek().getValue()))
.penalize("Penalize gaps between days", HardMediumSoftScore.ONE_SOFT, e -> e);
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Lukáš Petrovický |
Solution 2 |