Course timetabling
ここから「Real examples」です。要素が増えて複雑になってきます。
今回は講義を時間と部屋に割り振る最適化です。
- CourseSchedule : @PlanningSolution
- List
getPeriodList() : @ValueRangeProvider(id = "periodRange") - List
getRoomList() : @ValueRangeProvider(id = "roomRange") - List
getLectureList() : @PlanningEntityCollectionProperty
- List
- Lecture : @PlanningEntity
- Period : @PlanningVariable
- Room : @PlanningVariable
- Course course;
- int lectureIndexInCourse;
@PlanningVariable が2つになりました。search space も拡大します。講義(Lecture)とは、特定の時間(Period)と部屋(Room)で、講座(Course)を行う、という assignment です。モデリングでは語彙の把握が大事ですね。講座(Course)は講義(Lecture)に固定するので、PlanningVariableではありません。
Course は Teacher 及び Curriculum を持ちます。同じ Teacher が同時に講義を行うことはできない。同じ Curriculum を同じ時間にやってもいけない。
rule "conflictingLecturesDifferentCourseInSamePeriod" when $courseConflict : CourseConflict($leftCourse : leftCourse, $rightCourse : rightCourse) $leftLecture : Lecture(course == $leftCourse, $period : period, period != null) $rightLecture : Lecture(course == $rightCourse, period == $period, this != $leftLecture) then scoreHolder.addHardConstraintMatch(kcontext, - $courseConflict.getConflictCount()); end
おや、CourseConflict って何だ。CourseSchedule.precalculateCourseConflictList() で準備される、ルール用の便利factのようです。各2Courseの組み合わせに対して、Teacher及びCurriculumのコンフリクトがどれだけあるか、カウントして保持しておきます。で、同じ時間にぶつかった時、その conflictCount 分だけ hard score をマイナスします。ルール内でいちいち計算してたらパフォーマンスによろしくないということですね。
今回はさらに Selector もちょっと見てみよう。
<localSearch> <unionMoveSelector> <changeMoveSelector> <valueSelector> <variableName>period</variableName> </valueSelector> </changeMoveSelector> <changeMoveSelector> <valueSelector> <variableName>room</variableName> </valueSelector> </changeMoveSelector> <swapMoveSelector> <filterClass>org.optaplanner.examples.curriculumcourse.solver.move.DifferentCourseSwapMoveFilter</filterClass> </swapMoveSelector> </unionMoveSelector>
今までの多くの example では、selector の設定は省略されていました。省略するとデフォルトで changeMoveSelector + swapMoveSelector の union になります。ところがこの example では省略すると以下のように怒られます。
Exception in thread "main" java.lang.IllegalArgumentException: The selectorConfig (ValueSelectorConfig(null)) has no configured variableName (null) for entityClass (class org.optaplanner.examples.curriculumcourse.domain.Lecture) and because there are multiple variableNames ([period, room]), it can not be deducted automatically. at org.optaplanner.core.config.heuristic.selector.SelectorConfig.deduceVariableDescriptor(SelectorConfig.java:110) at org.optaplanner.core.config.heuristic.selector.value.ValueSelectorConfig.buildValueSelector(ValueSelectorConfig.java:198) at org.optaplanner.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig.buildBaseMoveSelector(ChangeMoveSelectorConfig.java:68) at org.optaplanner.core.config.heuristic.selector.move.MoveSelectorConfig.buildMoveSelector(MoveSelectorConfig.java:187) at org.optaplanner.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig.buildBaseMoveSelector(UnionMoveSelectorConfig.java:75) at org.optaplanner.core.config.heuristic.selector.move.MoveSelectorConfig.buildMoveSelector(MoveSelectorConfig.java:187) at org.optaplanner.core.config.localsearch.LocalSearchPhaseConfig.buildMoveSelector(LocalSearchPhaseConfig.java:143) ...
ChangeMoveSelector は、PlanningVariable がひとつだけのケースでは自動的にその variable を変更対象にするのですが、PlanningVariable が複数のケースでは決定できないので、ちゃんと config に書いてくれっていうことです。
一方 swapMoveSelector はデフォルトの
public class DifferentCourseSwapMoveFilter implements SelectionFilter<SwapMove> { public boolean accept(ScoreDirector scoreDirector, SwapMove move) { Lecture leftLecture = (Lecture) move.getLeftEntity(); Lecture rightLecture = (Lecture) move.getRightEntity(); return !leftLecture.getCourse().equals(rightLecture.getCourse()); } }
同じ Course の Lecture は swap しない、という filter です。スコア上意味が無いからですね。
では、filter ありとなしで、性能に差が出るのか。ベンチマークを走らせてみました。データは comp01 と comp02 のみ。terminationは 120 秒、late acceptance でやりました(短い時間では late acceptance の方が tabu search より好成績っぽい)。
- Without Filter
- comp01: 0hard/-12soft
- comp02: 0hard/-279soft
- With Filter
- comp01: 0hard/-18soft
- comp02: 0hard/-267soft
ちょっと興味深い結果になりました。データの少ない comp01 では Without Filter の方が、データの多い comp02 では With Filter の方が良い結果を出しました。もっと多くのデータ&長時間 termination で見ないと駄目ですが、必ずしもこの Filter が有効とは限らないようです。ただ、このベンチマークの結果だけで判断するなら、データの多い方の結果を重視して Filter 採用になるでしょう。