Search code examples
timefold

Writing constraints for nested (and dependent) planning entities


This is a follow up / updated question of May planning entites reference other planning entities?

I'm working on a school schedule solver.

In order to support "groups of lectures" with a common start time (important for "break out subjects" such as language classes, where spanish, german, french must be taught at the same time for the schedule to make sense), I have now created the following structure:

@PlaningEntity
class LectureGroup {

    @PlanningVariable
    Timeslot timeslot;

    List<Lecture> lectures;

    // ...some other properties...
}

@PlanningEntity
class Lecture {
    @PlanningVariable
    Room room;

    int lectureLength;
    LectureGroup myLectureGroup; // LectureGroup in which this lecture is part of

    // ...some other properties...

    Timeslot getStartTime() {
        return myLectureGroup.timeslot;
    }

    Timeslot getEndTime() {
        return myLectureGroup.timeslot.plus(lectureLength);
    }
}

@PlanningSolution
class Schedule {
 
    @PlanningEntityCollectionProperty
    List<LectureGroups> lectureGroups;

    @PlanningEntityCollectionProperty
    List<Lecture> lectures;  // always equal to flattened list of lectures from the lectureGroups

    // ...
}

To keep things simple, all Lectures are wrapped in a LectureGroup, even if the LectureGroup only contains one lecture.

My Question: (which is basically very similar to my previous one)

What's the recommended way to express, in this case, a room conflict constraint? A naive solution could look like:

UniConstraintStream<Lecture> lecturesWithRooms = cf.forEach(Lecture.class)
        .filter(Lecture::hasRoom);

return lecturesWithRooms.join(lecturesWithRooms,
            Joiners.equals(Lecture::getRoom),
            Joiners.overlapping(Lecture::getStartTime, Lecture::getEndTime)
            Joiners.lessThan(Lecture::getLectureId))
        .penalize(...)
        .justifyWith(...)
        .asConstraint(...);

As I understand, updates to the timeslot of a LectureGroup will not trigger proper reevaluation.

A refined solution could be to start with...

UniConstraintStream<Lecture> lecturesWithRooms = cf.forEach(LectureGroup.class)
        .filter(LectureGroup::hasTimeslot)
        .flattenLast(LectureGroup::getLectures)
        .filter(Lecture::hasRoom);

...but then I suspect direct updates to Lecture.room will not trigger reevaluation.

Reading the answer to my previous question, maybe I have to use both variants of lecturesWithRooms above, and join them with Function.identity in order to retrigger evaluation regardless if the time (in LectureGroup) or the room (in Lecture) is updated?

Any thoughts welcome here.


Update: Here's my current attempt:

UniConstraintStream<Lecture> lecturesFromLectures =
        cf.forEachIncludingNullVars(Lecture.class);

UniConstraintStream<Lecture> lecturesFromGroups =
        cf.forEachIncludingNullVars(LectureGroup.class).flattenLast(LectureGroup::getLectures);

UniConstraintStream<Lecture> lectures = lecturesFromLectures
        .join(lecturesFromGroups, Joiners.equal(Function.identity()))
        .groupBy((l1, l2) -> l1);

Solution

  • Performance considerations aside, this should work:

    BiConstraintStream<LectureGroup, Lecture> lectures = 
        cf.forEachIncludingNullVars(LectureGroup.class)
            .join(Lecture.class, Joiners.filtering((group, lecture) -> group.contains(lecture)));