OptaPlannerのガイド

OptaPlannerへのガイド

1. OptaPlannerの概要

このチュートリアルでは、OptaPlannerと呼ばれるJava制約満足度ソルバーについて説明します。

OptaPlannerは、最小限のセットアップで一連のアルゴリズムを使用して計画の問題を解決します。

アルゴリズムを理解することで役立つ詳細が得られるかもしれませんが、フレームワークは私たちのために大変な仕事をしています。

2. メーベン依存

まず、OptaPlannerのMaven依存関係を追加します。


    org.optaplanner
    optaplanner-core
    7.9.0.Final

OptaPlannerの最新バージョンをMaven Central repositoryから見つけます。

3. Problem/Solution Class

問題を解決するには、例として確かに特定の問題が必要です。

講義の時間割は、部屋、時間、教師などのリソースのバランスを取るのが難しいため、適切な例です。

3.1. CourseSchedule

CourseScheduleには、問題変数と計画エンティティの組み合わせが含まれているため、ソリューションクラスになります。 その結果、複数の注釈を使用して構成します。

それぞれを個別に詳しく見ていきましょう。

@PlanningSolution
public class CourseSchedule {

    private List roomList;
    private List periodList;
    private List lectureList;
    private HardSoftScore score;

PlanningSolution annotationは、このクラスにソリューションを含むデータが含まれていることをOptaPlannerに通知します。

OptaPlannerは、計画エンティティ、問題の事実、およびスコアという最小限のコンポーネントを期待しています。

3.2. Lecture

Lecture, POJOは、次のようになります。

@PlanningEntity
public class Lecture {

    public Integer roomNumber;
    public Integer period;
    public String teacher;

    @PlanningVariable(
      valueRangeProviderRefs = {"availablePeriods"})
    public Integer getPeriod() {
        return period;
    }

    @PlanningVariable(
      valueRangeProviderRefs = {"availableRooms"})
    public Integer getRoomNumber() {
        return roomNumber;
    }
}

計画エンティティとしてLectureクラスを使用するため、CourseScheduleのゲッターに別の注釈を追加します。

@PlanningEntityCollectionProperty
public List getLectureList() {
    return lectureList;
}

計画エンティティには、設定されている制約が含まれています。

PlanningVariableアノテーションとvalueRangeProviderRefアノテーションは、制約を問題の事実にリンクします。

これらの制約値は、すべての計画エンティティにわたって後でスコアリングされます。

3.3. 問題の事実

roomNumber 変数とperiod変数は、互いに同様に制約として機能します。

OptaPlannerは、これらの変数を使用したロジックの結果としてソリューションのスコアを付けます。 両方のgetterのメソッドにアノテーションを追加します。

@ValueRangeProvider(id = "availableRooms")
@ProblemFactCollectionProperty
public List getRoomList() {
    return roomList;
}

@ValueRangeProvider(id = "availablePeriods")
@ProblemFactCollectionProperty
public List getPeriodList() {
    return periodList;
}

これらのリストはすべて、Lecture fieldsで使用される可能性のある値です。

OptaPlannerは、検索スペース全体のすべてのソリューションにそれらを入力します。

最後に、各ソリューションにスコアを設定するため、スコアを保存するフィールドが必要です。

@PlanningScore
public HardSoftScore getScore() {
    return score;
}

スコアがないと、OptaPlannerは最適なソリューションを見つけることができないため、重要性を早期に強調します。

4. 得点

これまで見てきたこととは対照的に、スコアリングクラスにはより多くのカスタムコードが必要です。

これは、スコア計算機が問題とドメインモデルに固有であるためです。

4.1. カスタムJava

単純なスコア計算を使用して、この問題を解決します(ただし、このように思えない場合もあります)。

public class ScoreCalculator
  implements EasyScoreCalculator {

    @Override
    public Score calculateScore(CourseSchedule courseSchedule) {
        int hardScore = 0;
        int softScore = 0;

        Set occupiedRooms = new HashSet<>();
        for(Lecture lecture : courseSchedule.getLectureList()) {
            String roomInUse = lecture.getPeriod()
              .toString() + ":" + lecture.getRoomNumber().toString();
            if(occupiedRooms.contains(roomInUse)){
                hardScore += -1;
            } else {
                occupiedRooms.add(roomInUse);
            }
        }

        return HardSoftScore.valueOf(hardScore, softScore);
    }
}

上記のコードを詳しく見ると、重要な部分がより明確になります。 We calculate a score in the loop because the List<Lecture> contains specific non-unique combinations of rooms and periods.

HashSet isは、同じ部屋と期間で重複した講義にペナルティを課すことができるように、一意のキー(文字列)を保存するために使用されます。

その結果、ユニークな部屋と期間のセットを受け取ります。

4.2. よだれ

Droolsファイル-ファイルに適用するためのルールを簡単に変更する方法を提供します。 構文は時々混乱する可能性がありますが、Droolsファイルはコンパイルされたクラスの外部でロジックを管理する方法です。

nullエントリを防止するためのルールは次のようになります。

global HardSoftScoreHolder scoreHolder;

rule "noNullRoomPeriod"
    when
        Lecture( roomNumber == null );
        Lecture( period == null );
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1);
end

5. ソルバーの構成

別の必要な構成ファイル、ソルバーを構成するためのXMLファイルが必要です。

5.1. XML構成ファイル


    

    
        
            org.example.optaplanner.ScoreCalculator
        
    

    
        10
    

CourseScheduleクラスに注釈があるため、ここではscanAnnotatedClasses要素を使用してクラスパス上のファイルをスキャンします。

scoreDirectorFactory要素の内容は、スコアリングロジックを含むようにScoreCalculatorクラスを設定します。

Droolsファイルを使用する場合、要素のコンテンツを次のように置き換えます。

courseScheduleScoreRules.drl

最終的な設定は終了要素です。 存在しないかもしれない最適化されたソリューションを無限に検索するのではなく、この設定は制限時間後に検索を停止します。

ほとんどの問題には10秒で十分です。

6. テスト

ソリューション、ソルバー、問題クラスを構成しました。 テストしてみましょう!

6.1. テストのセットアップ

まず、いくつかのセットアップを行います。

SolverFactory solverFactory = SolverFactory
  .createFromXmlResource("courseScheduleSolverConfiguration.xml");
solver = solverFactory.buildSolver();

unsolvedCourseSchedule = new CourseSchedule();

次に、計画エンティティコレクションと問題ファクトListオブジェクトにデータを入力します。

6.2. テストの実行と検証

最後に、solveを呼び出してテストします。

CourseSchedule solvedCourseSchedule = solver.solve(unsolvedCourseSchedule);

assertNotNull(solvedCourseSchedule.getScore());
assertEquals(-4, solvedCourseSchedule.getScore().getHardScore());

solvedCourseScheduleに、「最適な」解があることを示すスコアがあることを確認します。

ボーナスとして、最適化されたソリューションを表示する印刷メソッドを作成します。

public void printCourseSchedule() {
    lectureList.stream()
      .map(c -> "Lecture in Room "
        + c.getRoomNumber().toString()
        + " during Period " + c.getPeriod().toString())
      .forEach(k -> logger.info(k));
}

このメソッドは以下を表示します。

Lecture in Room 1 during Period 1
Lecture in Room 2 during Period 1
Lecture in Room 1 during Period 2
Lecture in Room 2 during Period 2
Lecture in Room 1 during Period 3
Lecture in Room 2 during Period 3
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1

最後の3つのエントリがどのように繰り返されているかに注目してください。 これは、問題に対する最適な解決策がないために発生します。 3つの期間、2つの教室、10の講義を選択しました。

これらの固定リソースにより、可能な講義は6つのみです。 少なくともこの回答は、すべての講義を収容するのに十分な部屋や期間がないことをユーザーに示しています。

7. 追加機能

私たちが作成したOptaPlannerの例は単純なものでしたが、フレームワークはより多様なユースケースのための機能を追加しました。 最適化のためにアルゴリズムを実装または変更し、それを使用するフレームワークを指定する場合があります。

Javaのマルチスレッド機能の最近の改善により、OptaPlannerは開発者に、フォークと結合、インクリメンタルソルビング、マルチテナンシーなどのマルチスレッドの複数の実装を使用する機能も提供します。

詳細については、documentationを参照してください。

8. 結論

OptaPlannerフレームワークは、スケジューリングやリソース割り当てなどの制約充足問題を解決する強力なツールを開発者に提供します。

OptaPlannerは、JVMリソースの使用を最小限に抑え、Java EEと統合します。 著者はフレームワークを引き続きサポートしており、Red HatはそれをBusiness Rules Management Suiteの一部として追加しています。

いつものように、コードはover on Githubで見つけることができます。