I have the following dataset, and am trying to work out how to best write a specific constraint.
My PlanningEntity
looks (roughly) as follows:
@PlanningEntity
public class Participation {
@PlanningId
private long id;
private Student student;
private Lesson lesson;
@PlanningVariable(valueRangeProviderRefs = "possibleEnrollments")
private Boolean enrolled;
}
whereby a Lesson
has a public List<Subject> getSubjects()
(note: a list of multiple subjects).
What I would like to do in my penalize
method is look at all participations of a student/subject (single subject!) combination. In other words, if I have lesson A
with subject 1
, lesson B
with subject 2
and lesson C
with subjects 1
and 2
, I would like to do a grouping in such a way that in my penalize function I get two (Student, List<Participation>)
callbacks: one for subject 1
and one for subject 2
, whereby the first lists contains lessons A
and C
, and the second list has lessons B
and C
. So, C is contained in two lists.
The following does not work:
constraintFactory
.forEach(Participation.class)
.groupBy(Participation::getStudent, Participation::getSubjects, toList())
since this groups on the entire List
returned by Participation::getSubject
and the set 1 and 2
attached to lesson C
becomes a separate group.
I have currently 'solved' the problem as follows, with a custom UniConstraintCollection
:
return constraintFactory
.forEach(Participation.class)
.groupBy(Participation::getStudent, new UniConstraintCollector<Participation, Map<Subject, List<Participation>>, Map<Subject, List<Participation>>>() {
@Override
public Supplier<Map<Subject, List<Participation>>> supplier() {
return HashMap::new;
}
@Override
public BiFunction<Map<Subject, List<Participation>>, Participation, Runnable> accumulator() {
return (map, participation) -> {
for(Subject s : participation.getSubjects()) {
if(!map.containsKey(s)) {
map.put(s, new ArrayList<>());
}
map.get(s).add(participation);
}
return () -> {
for(Map.Entry<Subject, List<Participation>> entry: map.entrySet()) {
entry.getValue().remove(participation);
}
};
};
}
@Override
public Function<Map<Subject, List<Participation>>, Map<Subject, List<Participation>>> finisher() {
return Function.identity();
}
}).penalize(("name", HardSoftScore.ONE_SOFT, (student, participationMap) -> {
...
});)
This works, in that I receive a map of Subject
to Participations
and allows me to calculate the penalty I want.
However, this means that I calculate one penalty value for all subject/list combinations 'together'. From a usability perspective, I would like to penalize each subject/list separately. Is there a way to do this? (Maybe by rewriting the UniConstraintCollector
to provide multiple lists of Participations
, instead of one single Map<Subject, List<Participation>>
?)
P.S. Another approach I have tried to achieve the same goal is to work from the perspective of the Subject
. So, making Subject
s a ProblemFact on the PlanningSolution, and working with something like
return constraintFactory
.forEach(Subject.class)
.join(constraintFactory.forEach(Participation.class),
JoinerSupport.getJoinerService().newBiJoiner(List::of, JoinerType.INTERSECTING, Participation::getSubjects)
)
I assume this is the way I would need to go, using the JoinerType INTERSECTING, but this gives me an "Unsupported Joiner Type" exception in AbstractLeftHandSide
.
What if you start from Subject?
constraintFactory
.forEach(Subject.class)
.join(Participation.class,
// Joiners.containedBy() would do this far more efficient
filtering((s, p) -> p.getSubjects().contains(s))
.groupBy((s, p) -> s, toList((s, p) -> p))
This could be an expensive constraint performance wise. Benchmark it.