Red Hat build of OptaPlanner を使用したソルバーの開発


Red Hat Build of OptaPlanner 8.33

概要

このドキュメントでは、Red Hat build of OptaPlanner を使用してソルバーを開発し、計画の問題に対する最適なソリューションを見つける方法について説明します。

はじめに

Red Hat build of OptaPlanner を使用して、計画問題に対する最適解を決定するソルバーを開発できます。OptaPlanner は、Red Hat build of OptaPlanner の組み込みコンポーネントです。Red Hat build of OptaPlanner のサービスの一部としてソルバーを使用して、特定の制約のある限られたリソースを最適化できます。

多様性を受け入れるオープンソースの強化

Red Hat では、コード、ドキュメント、Web プロパティーにおける配慮に欠ける用語の置き換えに取り組んでいます。まずは、マスター (master)、スレーブ (slave)、ブラックリスト (blacklist)、ホワイトリスト (whitelist) の 4 つの用語の置き換えから始めます。この取り組みにより、これらの変更は今後の複数のリリースに対して段階的に実施されます。詳細は、Red Hat CTO である Chris Wright のメッセージ をご覧ください。

パート I. Red Hat build of OptaPlanner 8.33 のリリースノート

これらのリリースノートでは、Red Hat build of OptaPlanner 8.33 の新機能を表示し、アップグレード手順が記載されています。

第1章 OptaPlanner 8.13 から Red Hat build of OptaPlanner 8.33 へのアップグレード

OptaPlanner 8.13 から Red Hat build of OptaPlanner 8.33 にアップグレードするには、OptaPlanner の以前のバージョンを次の順序でマージします。

手順

  1. ブラウザーで OptaPlanner Upgrade Recipe 8 ページを開きます。
  2. アップグレードする最初のバージョンの手順を完了します (たとえば 8.13.0.Final から 8.14.0.Final)
  3. 8.32.0.Final にアップグレードされるまで、手順を繰り返します。

第2章 Red Hat build of OptaPlanner 8.33 の新機能

このセクションでは、Red Hat build of OptaPlanner 8.33 の新機能について説明します。

注記

Bavet は、高速スコア計算に使用される機能です。Bavet は現在、OptaPlanner のコミュニティーバージョンでのみ利用できます。Red Hat build of OptaPlanner 8.33 では使用できません。

2.1. OptaPlanner および Red Hat build of Quarkus プラットフォーム

Red Hat build of OptaPlanner は、Red Hat build of Quarkus プラットフォームに統合されるようになりました。OptaPlanner の依存関係を含むプラットフォームアーティファクトの依存関係のバージョンは、Quarkus bill of materials (BOM)ファイル com.redhat.quarkus.platform:quarkus-bom で維持されます。OptaPlanner プロジェクトで Quarkus BOM ファイルを使用する場合は、どの依存関係バージョンが連携するかを指定する必要はありません。代わりに、Quarkus BOM ファイルを pom.xml 設定ファイルにインポートできます。依存関係のバージョンは <dependencyManagement> セクションに含まれています。そのため、pom.xml ファイルの指定の BOM で管理される個別の Quarkus 依存関係のバージョンを記述する必要はありません。

2.2. 値範囲の自動検出

ほとんどの場合、プランニング変数と値範囲間のリンクは自動検出されます。そのため、@ValueRangeProvider アノテーションは id プロパティーを提供する必要がなくなりました。同様に、プランニング変数は、valueRangeProviderRefs プロパティーを介して値の範囲プロバイダーを参照する必要がなくなりました。

コードや設定変更は必要ありません。簡潔さよりも明確さを優先する場合は、引き続き値範囲プロバイダーを明示的に参照できます。

2.3. XStream サポートが非推奨に

XStream を使用した XML へのシリアル化に対する OptaPlanner のサポートは、このリリースでは非推奨になりました。optaplanner-persistence-xstream モジュールと、それに含まれるすべてのクラスは非推奨となり、Red Hat build of OptaPlanner の今後のリリースで削除されます。XML へのシリアライズを続行するには、optaplanner-persistence-jaxb モジュールに切り替えます。

optaplanner-examples モジュール内のすべての例は optaplanner-persistance-jackson モジュールを使用して JSON にリファクタリングされています。クイックスタートは、すでに JSON にシリアライズされているため、今回の変更の影響を受けます。

2.4. OptaPlanner サンプルがディストリビューションから削除

以下の例は、optaplanner-examples ディレクトリーから削除されました。

  • Batch Scheduling
  • Cheap Time
  • Coach Shuttle Gathering
  • 投資
  • Rock Tour

第3章 Red Hat build of OptaPlanner 8.33 で修正された問題

Red Hat build of OptaPlanner 8.33 では安定性が増し、本セクションに挙げる問題が修正されました。

  • ドメインに、チェーン変数が含まれるエンティティーが複数ある場合は、ClassCastException [PLANNER-2798] で実行時に失敗します。
  • CS-D ストリームの再利用に失敗する [PLANNER-2884]
  • subList 取り消しを最適化する [PLANNER-2808]
  • 未解決の依存関係: org.optaplanner:optaplanner-constraint-streams-bavet:jar:8.29.0.Final-redhat-00009 [RHBOP-33]

パート II. Red Hat build of OptaPlanner のスタートガイド

ビジネスルールの開発者は、Red Hat build of OptaPlanner を使用して、限られたリソースや個別の制約の中で計画問題に対する最適解を見つけ出すことができます。

本書を使用して、Red Hat build of OptaPlanner で Solver の開発を開始していきます。

第4章 Red Hat build of OptaPlanner の概要

OptaPlanner は組み込み可能な軽量プランニングエンジンで、プランニングの問題を最適化します。最適化のためのヒューリスティック法およびメタヒューリスティック法を、非常に効率的なスコア計算と組み合わせ、一般的な Java プログラマーが計画問題を効率的に解決できるようにします。

たとえば、OptaPlanner は、さまざまなユースケースの解決に役立ちます。

  • 従業員勤務表/患者一覧: 看護師の勤務シフト作成を容易にし、病床管理を追跡します。
  • 教育機関の時間割: 授業、コース、試験、および会議の計画を容易にします。
  • 工場の計画: 自動車の組み立てライン、機械の操業計画、および作業員のタスク計画を追跡します。
  • 在庫の削減: 紙や金属などの資源の消費を減らし、無駄を最小限に抑えます。

どの組織も、制約のある限定されたリソース (従業員、資産、時間、および資金) を使用して製品やサービスを提供するといった計画問題に直面しています。

OptaPlanner は、Apache Software License 2.0 を使用するオープンソースソフトウェアです。100% Pure Java に認定されており、ほとんどの Java 仮想マシン (JVM) で稼働します。

4.1. Backwards compatibility

OptaPlanner は API と実装を分離します。

  • パブリック API : パッケージ名前空間 org.optaplanner.core.apiorg.optaplanner.benchmark.apiorg.optaplanner.test.api および org.optaplanner.persistence.api 内のすべてのクラスは、今後のマイナーおよびパッチリリースで 100% 下位互換性があります。まれに、メジャーバージョン番号が変更されると、いくつかの特定のクラスに下位互換性のない変更がいくつか含まれることがありますが、そのような変更は アップグレードレシピ に明確に文書化されます。
  • XML 設定: XML ソルバー設定は、非パブリック API クラスの使用を必要とする要素以外の全要素に対して下位互換性があります。XML Solver 設定は、パッケージ名前空間 org.optaplanner.core.config および org.optaplanner.benchmark.config のクラスによって定義されます。
  • 実装クラス: その他のクラスはすべて後方互換性が ありません。これらは将来のメジャーリリースまたはマイナーリリースで変更される予定です。アップグレードレシピ では、関連する変更点、新しいバージョンへのアップグレードするときにそのような変更に対応する方法を説明します。

4.2. 計画問題

計画問題 では、限られたリソースや個別の制約の中で最適なゴールを見つけ出します。最適なゴールは、次のようなさまざまなものです。

  • 最大の利益: 最適なゴールにより、可能な限り高い利益が得られます。
  • 経済活動の最小フットプリント: 最適なゴールでは、環境負荷が最小となります。
  • スタッフ/顧客の最大満足: 最適なゴールでは、スタッフ/顧客のニーズが優先されます。

これらのゴールに到達できるかどうかは、利用できるリソースの数に依存します。たとえば、以下のようなリソースには制限があります。

  • ユーザー数
  • 時間
  • 予算
  • 装置、車両、コンピューター、施設などの物理資産

これらのリソースに関連する個別の制約についても考慮する必要があります。たとえば、要員が働くことのできる時間数、特定の装置を使用することのできる技能、または機器同士の互換性などです。

Red Hat build of OptaPlanner は、Java プログラマーが制約の飽和性の問題を効率的に解決するのに役立ちます。最適化ヒューリスティックとメタヒューリスティックを効率的なスコア計算と組み合わせます。

4.3. 計画問題での NP 完全

例に挙げたユースケースは 通常 NP 完全または NP 困難 であり、以下のことが言えます。

  • 問題に対する解を実用的な時間内に検証することが容易です。
  • 問題に対する最適解を実用的な時間内に見つけ出す確実な方法がない。

この場合、一般的な 2 つの手法では不十分であるため、問題を解くのが予想より困難だと考えられます。

  • 力まかせアルゴリズムでは (より高度な類似アルゴリズムであっても)、時間がかかり過ぎる。
  • たとえば ビンパッキング問題の ような迅速なアルゴリズムでは、容量の大きい順でアイテムを入力すると、最適とはほど遠い解が返されます。

高度な最適化アルゴリズムを用いる OptaPlanner であれば、このような計画問題に対する適切な解を、妥当な時間内に見つけ出すことができます。

4.4. 計画問題に対する解

計画問題には、多数の解が存在します。

以下に示すように、解は複数のカテゴリーに分類されます。

可能解
可能解とは、制約に違反するかどうかは問わず、あらゆる解を指します。通常、計画問題には膨大な数の可能解が存在します。ただし、このような解の多くは、役に立ちません。
実行可能解
実行可能解とは、いずれの (負の) ハード制約にも違反しない解を指します。実行可能解の数は、可能解の数に比例します。実行可能解が存在しないケースもあります。実行可能解は、可能解の部分集合です。
最適解
最適解とは、最高スコアの解を指します。通常、計画問題には数個の最適解が存在します。実行可能解が存在せず、最適解が現実的ではない場合でも、計画問題には少なくとも 1 つの最適解が必ず存在します。
見つかった最善解
最善解とは、指定された時間内に実施した検索で見つかった最高スコアの解を指します。通常、見つかった最善解は現実的で、十分な時間があれば最適解を見つけることができます。

直観には反していますが、小規模なデータセットの場合であっても、(正しく計算された場合は) 膨大な数の可能解が存在します。

optaplanner-examples/src 配布フォルダーで提供される例では、ほとんどのインスタンスに多数の可能解が存在します。最適解を確実に見つけることができる方法は存在しないため、いかなる実行方法も、これらすべての可能解の部分集合を評価することしかできません。

膨大な数の可能解全体を効率的に網羅するために、OptaPlanner はさまざまな最適化アルゴリズムをサポートしています。

ユースケースによっては、ある最適化アルゴリズムが他のアルゴリズムより勝ることがありますが、それを事前に予測することは不可能です。OptaPlanner では、XML またはコード中の Solver 設定を数行変更するだけで、最適化アルゴリズムを切り替えることができます。

4.5. 計画問題に対する制約

通常、プランニングの問題には、少なくとも 2 つの制約レベルがあります。

  • (負の) ハード制約 は、絶対に違反してはならない。

    例: 1 人の教師は同時に 2 つの講義を受け持つことはできない。

  • (負の) ソフト制約 は、避けることが可能であれば違反してはならない。

    例: 教師 A は金曜日の午後に講義を受け持ちたくない。

正の制約を持つ問題もあります。

  • 正のソフト制約 (ボーナス) は、可能であれば満たす必要がある。

    例: 教師 B は月曜日の午前中に講義を受け持つことを希望している。

一部の基本的な問題にはハード制約のみがあります。問題によっては、3 つ以上の制約があります (例: ハード制約、中程度の制約、ソフト制約)。

これらの制約により、計画問題における スコア計算方法 (または 適合度関数) が定義されます。プランニングの問題の解は、それぞれスコアで等級付けすることができます。OptaPlanner のスコア制約は、Java などのオブジェクト指向言語または Drools ルールで記述されます。

このタイプのコードは柔軟で、スケーラビリティーに優れます。

4.6. Red Hat build of OptaPlanner で提供される例

Red Hat build of OptaPlanner には、OptaPlanner のサンプルが複数同梱されています。たとえばコードなどを確認して、ニーズに合ったものに変更できます。

注記

Red Hat は、Red Hat build of OptaPlanner ディストリビューションに含まれるコードサンプルのサポートはしていません。

OptaPlanner サンプルには、教育関連のコンテストで出題された問題を解決するものもあります。以下の表の Contest 列には、このようなコンテストが掲載されています。また、コンテストの目的として、現実的 か、非現実的 かの識別をしています。現実的なコンテスト とは、以下の基準を満たす、独立した公式コンテストを指します。

  • 明確に定義された実際のユースケースであること
  • 実際に制約があること
  • 実際のデータセットが複数あること
  • 特定のハードウェアで特定の時間内に結果を再現できること
  • 教育機関および/または企業の運用研究コミュニティーが真剣に参加していること

現実的なコンテストでは、競合のソフトウェアや教育研究と OptaPlanner を客観的に比較できます。

表4.1 サンプルの概要
ドメインサイズコンテストディレクトリー名

N クィーン

エンティティークラス 1 つ

(変数 1 つ)

エンティティー ⇐ 256

値 ⇐ 256

探索空間 ⇐ 10^616

無意味 (不正が可能)

nqueens

クラウドバランシング

エンティティークラス 1 つ

(変数 1 つ)

エンティティー ⇐ 2400

値 ⇐ 800

探索空間 ⇐ 10^6967

いいえ (弊社が定義)

cloudbalancing

巡回セールスマン

エンティティークラス 1 つ

(連鎖変数 1 つ)

エンティティー ⇐ 980

値 ⇐ 980

探索空間 ⇐ 10^2504

現実的でない TSP Web

tsp

テニスクラブのスケジュール

エンティティークラス 1 つ

(変数 1 つ)

エンティティー ⇐ 72

値 ⇐ 7

探索空間 ⇐ 10^60

いいえ (弊社が定義)

tennis

会議のスケジュール

エンティティークラス 1 つ

(変数 2 つ)

エンティティー ⇐ 10

値 ⇐ 320 および ⇐ 5

探索空間 ⇐ 10^320

いいえ (弊社が定義)

meetingscheduling

コースの時間割

エンティティークラス 1 つ

(変数 2 つ)

エンティティー ⇐ 434

値 ⇐ 25 および ⇐ 20

探索空間 ⇐ 10^1171

現実的 ITC 2007 track 3

curriculumCourse

マシンの再割当て

エンティティークラス 1 つ

(変数 1 つ)

エンティティー ⇐ 50000

値 ⇐ 5000

探索空間 ⇐ 10^184948

ほぼ現実的 ROADEF 2012

machineReassignment

配送経路

エンティティークラス 1 つ

(連鎖変数 1 つ)

シャドウエンティティークラス 1 つ

(自動シャドウ変数 1 つ)

エンティティー ⇐ 55

値 ⇐ 2750

探索空間 ⇐ 10^8380

現実的でない VRP Web

vehiclerouting

時間枠がある中での 配送経路

配送経路すべて

(シャドウ変数 1 つ)

エンティティー ⇐ 55

値 ⇐ 2750

探索空間 ⇐ 10^8380

現実的でない VRP Web

vehiclerouting

プロジェクトジョブのスケジュール

エンティティークラス 1 つ

(変数 2 つ)

(シャドウ変数 1 つ)

エンティティー ⇐ 640

値 ⇐ ? および ⇐ ?

探索空間 ⇐ ?

ほぼ現実的 MISTA 2013

projectjobscheduling

タスクの割り当て

エンティティークラス 1 つ

(リスト変数 1つ)

シャドウエンティティークラス 1 つ

(自動シャドウ変数 1 つ)

(シャドウ変数 1 つ)

エンティティー ⇐ 20

値 ⇐ 500

探索空間 ⇐ 10^1168

いいえ (弊社が定義)

taskassigning

試験の時間割

エンティティークラス 2 つ (同じ階層)

(変数 2 つ)

エンティティー ⇐ 1096

値 ⇐ 80 および ⇐ 49

探索空間 ⇐ 10^3374

現実的 ITC 2007 track 1

examination

看護師の勤務表

エンティティークラス 1 つ

(変数 1 つ)

エンティティー ⇐ 752

値 ⇐ 50

探索空間 ⇐ 10^1277

現実的 INRC 2010

nurserostering

巡回トーナメント

エンティティークラス 1 つ

(変数 1 つ)

エンティティー ⇐ 1560

値 ⇐ 78

探索空間 ⇐ 10^2301

現実的でない TTP

travelingtournament

会議スケジュール

エンティティークラス 1 つ

(変数 2 つ)

エンティティー ⇐ 216

値 ⇐ 18 および ⇐ 20

探索空間 ⇐ 10^552

いいえ (弊社が定義)

conferencescheduling

航空機乗組員のスケジューリング

エンティティークラス 1 つ

(変数 1 つ)

シャドウエンティティークラス 1 つ

(自動シャドウ変数 1 つ)

エンティティー ⇐ 4375

値 ⇐ 750

探索空間 ⇐ 10^12578

いいえ (弊社が定義)

flightcrewscheduling

4.7. N クィーン

n サイズのチェスボードに、他のクィーンに取られないクィーンに n 個のクィーンを置きます。最も一般的な n クィーンパズルは、n = 8 の 8 個のクィーンパズルです。

N クイーン問題の図解

制約:

  • n 列および n 行のチェスボードを使用します。
  • チェスボードに n 個のクィーンを置きます。
  • クィーンが他のクィーンに取られないように配置します。クィーンは、同じ水平線上、垂直線上、対角線上にある他のクィーンを取ることができます。

本書では、4 つのクイーンパズルを主な例として多用しています。

以下が提案された解です。

図4.1 4 個のクィーンパズルの誤った解

4 個のクィーンパズルの誤った解

上記の解は、A1B0 (および B0D0) のクィーンがお互いに駒を取れるので間違っています。B0 のクィーンをどかせば他のクィーンに取られないようにするという制約は順守できますが、n 個のクィーンを置くという制約に違反します。

以下は正しい解です。

図4.2 クィーン 4 個のパズルの正しい解

クィーン 4 個のパズルの正しい解

すべての制約が満たされているので、これが正解です。

n クィーンパズルでは、正解が複数存在する場合が多々あります。特定の n に対して考えられる解を見つけるのではなく、特定の n に対する正しい解を 1 つ導き出すことにフォーカスします 。

問題の規模

4queens   has   4 queens with a search space of    256.
8queens   has   8 queens with a search space of   10^7.
16queens  has  16 queens with a search space of  10^19.
32queens  has  32 queens with a search space of  10^48.
64queens  has  64 queens with a search space of 10^115.
256queens has 256 queens with a search space of 10^616.

n クィーンは、初心者用のサンプルとして実装されているため、最適化はされていません。それにもかかわらず、クィーンが 64 個になっても簡単に処理できます。何点か変更を加えると、クィーンが 5000 個以上になっても簡単に対応できることが立証されています。

4.7.1. N クィーンのドメインモデル

この例では、4 つのクィーンの問題を解決するドメインモデルを使用します。

  • ドメインモデルの作成

    適切なドメインモデルを使用すると、プランニングの問題をより簡単に理解し、解決することができます。

    以下は、n クィーンの例のドメインモデルです。

    public class Column {
    
        private int index;
    
        // ... getters and setters
    }
    public class Row {
    
        private int index;
    
        // ... getters and setters
    }
    public class Queen {
    
        private Column column;
        private Row row;
    
        public int getAscendingDiagonalIndex() {...}
        public int getDescendingDiagonalIndex() {...}
    
        // ... getters and setters
    }
  • 探索空間の計算

    Queen インスタンスには Column (例: 0 は列 A、1 は列 B​) および Row (例: 0 は行 0、1 は行 1​) が含まれます。

    列と行をもとに、昇順の対角線、および降順の対角線を計算することができます。

    列と行のインデックスは、チェスボードの左上隅から数えています。

    public class NQueens {
    
        private int n;
        private List<Column> columnList;
        private List<Row> rowList;
    
        private List<Queen> queenList;
    
        private SimpleScore score;
    
        // ... getters and setters
    }
  • 解の求め方

    1 つの NQueens インスタンスには Queen インスタンスの一覧が含まれています。これが Solution 実装として提供され、Solver が解決して読み出します。

たとえば、4 クイーンのサンプルでは、NQueens の getN() メソッドが常に 4 を返します。

図4.3 クィーン 4 個の解

クィーン 4 個の解
表4.2 ドメインモデルの解の詳細
 columnIndexrowIndexascendingDiagonalIndex (columnIndex + rowIndex)descendingDiagonalIndex (columnIndex - rowIndex)

A1

0

1

1 (**)

-1

B0

1

0 (*)

1 (**)

1

C2

2

2

4

0

D0

3

0 (*)

3

3

(*) や (**) のように、クィーン 2 つが同じ行、列、対角線を共有する場合は、2 つの駒が互いを取ることができます。

4.8. クラウドバランシング

この例に関する詳細は、Red Hat build of OptaPlanner クイックスタートガイド を参照してください。

4.9. 巡回セールスマン (TSP - 巡回セールスマン問題)

都市の一覧をもとに、セールスマンが最短距離で、各都市を 1 度だけ訪問するルートを探します。

この問題は ウィキペディア に定義されています。これは、計算数学で 最も熱心に研究された問題の 1 つ です。大概は、従業員のシフト勤務など、その他の制約と一緒に計画の問題の一部として使用されます。

問題の規模

dj38     has  38 cities with a search space of   10^43.
europe40 has  40 cities with a search space of   10^46.
st70     has  70 cities with a search space of   10^98.
pcb442   has 442 cities with a search space of  10^976.
lu980    has 980 cities with a search space of 10^2504.

問題の難易度

TSP の定義は単純ですが、問題の解決は驚くほど難しくなります。これは NP 困難問題と呼ばれ、多くの計画の問題と同様、特定の問題のデータセットに対する最適な解は、その問題のデータセットが少しでも変更すると、大幅に変化する可能性があります。

巡回販売員の最適解の変動性。新しい場所を 1 つ追加すると最適解が大きく変化することを示します。

4.10. テニスクラブのスケジュール

テニスクラブでは、毎週 4 チームが総あたりで試合をします。4 つの対戦枠を公平にチームに割り当てます。

ハード制約:

  • 競合: チームは 1 日に 1 回だけ試合ができる。
  • 参加不可: 日程によって参加できないチームがある。

中程度の制約:

  • 公平な割り当て: 各チームが試合をする回数を (ほぼ) 同じにする。

ソフト制約:

  • 均等に対戦: 各チームが、各対戦相手と対戦する回数を同じにする。

問題の規模

munich-7teams has 7 teams, 18 days, 12 unavailabilityPenalties and 72 teamAssignments with a search space of 10^60.

図4.4 ドメインモデル

テニスクラス図

4.11. 会議のスケジュール

各会議に、開始時間と会議室を割り当てます。会議の長さは異なります。

ハード制約:

  • 部屋の制約: 2 つの会議が、同じ時間に同じ会議室を使用することはできない。
  • 必須の出席者: 同じ時間に開催される必須の会議を 2 つ割り当てることはできない。
  • 必要とされる部屋の収容人数: 会議の出席者全員を収容できない部屋では会議を行ってはいけない。
  • 同日中に開始して終了: 会議は複数の日にわたってスケジュールされないようにする。

中程度の制約:

  • 任意の出席者: 同じ時間に開催される任意の会議を 2 つ割り当てることはできない。また、任意の会議と必須の会議を同じ時間に割り当てることはできない。

ソフト制約:

  • 早い段階でスケジュール: すべての会議をできるだけ早くスケジュールする。
  • 会議と会議の間の休憩時間: 会議と会議の間には、最低でも時間枠 1 つ分、休憩を入れる必要がある。
  • 会議の重複: 並行して行われる会議の数を最小限に抑えて、どちらかの会議を選択しなければならない状況をなくす。
  • 先に大きい部屋から割り当てる: 参加者が登録していない場合でも、できるだけ多数の参加者を収容するために、大きい部屋が空いている場合にはその部屋から割り当てていく必要がある。
  • 部屋の不変性: 会議が連続して行われ、休憩の時間枠が 2 つ分より少ない場合には、会議は同じ部屋で行う方が良い。

問題の規模

50meetings-160timegrains-5rooms  has  50 meetings, 160 timeGrains and 5 rooms with a search space of 10^145.
100meetings-320timegrains-5rooms has 100 meetings, 320 timeGrains and 5 rooms with a search space of 10^320.
200meetings-640timegrains-5rooms has 200 meetings, 640 timeGrains and 5 rooms with a search space of 10^701.
400meetings-1280timegrains-5rooms has 400 meetings, 1280 timeGrains and 5 rooms with a search space of 10^1522.
800meetings-2560timegrains-5rooms has 800 meetings, 2560 timeGrains and 5 rooms with a search space of 10^3285.

4.12. コースの時間割 (ITC 2007 Track 3 - カリキュラムのスケジュール)

各授業を、時間枠および講義室に割り当ててスケジュールを組みます。

ハード制約:

  • 講師の制約: 各講師は、同じ時間に授業を 2 つ受け持つことはできない。
  • カリキュラムの制約: カリキュラムには、2 つの授業を同じ時間に設定することはできない。
  • 部屋の占有: 同じ時間の同じ講義室に、2 つの授業を割り当てることはできない。
  • 利用不可の時間 (データセットごとに指定): 授業には割り当てられない時間がある。

ソフト制約:

  • 講義室の収容人数: 講義室の収容人数は、その授業を受ける学生の数よりも多くなければならない。
  • 最小限の就業日数: 同じコースの授業の開講期間は、最短になるようにする。
  • カリキュラムの緊密さ: 同じカリキュラムに含まれる授業は、時間帯を近く (連続した時間に) 設定する。
  • 講義室の不変性: 同じコースの授業は同じ講義室を割り当てる必要がある。

この問題は、International Timetabling Competition 2007 track 3 で定義されています。

問題の規模

comp01 has 24 teachers,  14 curricula,  30 courses, 160 lectures, 30 periods,  6 rooms and   53 unavailable period constraints with a search space of  10^360.
comp02 has 71 teachers,  70 curricula,  82 courses, 283 lectures, 25 periods, 16 rooms and  513 unavailable period constraints with a search space of  10^736.
comp03 has 61 teachers,  68 curricula,  72 courses, 251 lectures, 25 periods, 16 rooms and  382 unavailable period constraints with a search space of  10^653.
comp04 has 70 teachers,  57 curricula,  79 courses, 286 lectures, 25 periods, 18 rooms and  396 unavailable period constraints with a search space of  10^758.
comp05 has 47 teachers, 139 curricula,  54 courses, 152 lectures, 36 periods,  9 rooms and  771 unavailable period constraints with a search space of  10^381.
comp06 has 87 teachers,  70 curricula, 108 courses, 361 lectures, 25 periods, 18 rooms and  632 unavailable period constraints with a search space of  10^957.
comp07 has 99 teachers,  77 curricula, 131 courses, 434 lectures, 25 periods, 20 rooms and  667 unavailable period constraints with a search space of 10^1171.
comp08 has 76 teachers,  61 curricula,  86 courses, 324 lectures, 25 periods, 18 rooms and  478 unavailable period constraints with a search space of  10^859.
comp09 has 68 teachers,  75 curricula,  76 courses, 279 lectures, 25 periods, 18 rooms and  405 unavailable period constraints with a search space of  10^740.
comp10 has 88 teachers,  67 curricula, 115 courses, 370 lectures, 25 periods, 18 rooms and  694 unavailable period constraints with a search space of  10^981.
comp11 has 24 teachers,  13 curricula,  30 courses, 162 lectures, 45 periods,  5 rooms and   94 unavailable period constraints with a search space of  10^381.
comp12 has 74 teachers, 150 curricula,  88 courses, 218 lectures, 36 periods, 11 rooms and 1368 unavailable period constraints with a search space of  10^566.
comp13 has 77 teachers,  66 curricula,  82 courses, 308 lectures, 25 periods, 19 rooms and  468 unavailable period constraints with a search space of  10^824.
comp14 has 68 teachers,  60 curricula,  85 courses, 275 lectures, 25 periods, 17 rooms and  486 unavailable period constraints with a search space of  10^722.

図4.5 ドメインモデル

カリキュラムコースクラス図

4.13. マシンの再割当て (Google ROADEF 2012)

各プロセスをマシンに割り当てます。全プロセスには、すでに元の (最適化されていない) 割り当てがあります。プロセスにはそれぞれ、各リソース (CPU、メモリーなど) が一定量必要です。これは、クラウドのバランスの例の応用です。

ハード制約:

  • 最大容量: マシンに割り当てる各リソースはこの量を超えてはいけない。
  • 競合: 同じサービスのプロセスは別のマシンで実行する必要がある。
  • 分散: 同じサービスのプロセスは複数の場所に分散させる必要がある。
  • 依存関係: 他のサービスに依存するサービスのプロセスは、そのサービスの近くで実行する必要がある。
  • 一時的な使用: リソースによっては一時的なものがあり、元のマシンと、新たに割り当てられたマシンの両方の最大容量にカウントされる。

ソフト制約:

  • 負荷: 各マシンの各リソースの安全容量を超えてはいけない。
  • 負荷分散: 各マシンで利用可能なリソースを分散させて、今後の割り当てに対応できるように容量を空ける。
  • プロセスの移動コスト: プロセスには移動コストが発生する。
  • サービスの移動コスト: サービスには移動コストが発生する。
  • 機械の移動コスト: マシン A からマシン B にプロセスを移動すると、A から B に固有の移動コストが別途発生する。

この問題は the Google ROADEF/EURO Challenge 2012 で定義されています。

クラウドの最適化をテトリスゲームと比較した図

図4.6 価値提案

プロセスをより効率的にマシンに割り当てる方法を示す図

問題の規模

model_a1_1 has  2 resources,  1 neighborhoods,   4 locations,    4 machines,    79 services,   100 processes and 1 balancePenalties with a search space of     10^60.
model_a1_2 has  4 resources,  2 neighborhoods,   4 locations,  100 machines,   980 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a1_3 has  3 resources,  5 neighborhoods,  25 locations,  100 machines,   216 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a1_4 has  3 resources, 50 neighborhoods,  50 locations,   50 machines,   142 services,  1000 processes and 1 balancePenalties with a search space of   10^1698.
model_a1_5 has  4 resources,  2 neighborhoods,   4 locations,   12 machines,   981 services,  1000 processes and 1 balancePenalties with a search space of   10^1079.
model_a2_1 has  3 resources,  1 neighborhoods,   1 locations,  100 machines,  1000 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a2_2 has 12 resources,  5 neighborhoods,  25 locations,  100 machines,   170 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a2_3 has 12 resources,  5 neighborhoods,  25 locations,  100 machines,   129 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a2_4 has 12 resources,  5 neighborhoods,  25 locations,   50 machines,   180 services,  1000 processes and 1 balancePenalties with a search space of   10^1698.
model_a2_5 has 12 resources,  5 neighborhoods,  25 locations,   50 machines,   153 services,  1000 processes and 0 balancePenalties with a search space of   10^1698.
model_b_1  has 12 resources,  5 neighborhoods,  10 locations,  100 machines,  2512 services,  5000 processes and 0 balancePenalties with a search space of  10^10000.
model_b_2  has 12 resources,  5 neighborhoods,  10 locations,  100 machines,  2462 services,  5000 processes and 1 balancePenalties with a search space of  10^10000.
model_b_3  has  6 resources,  5 neighborhoods,  10 locations,  100 machines, 15025 services, 20000 processes and 0 balancePenalties with a search space of  10^40000.
model_b_4  has  6 resources,  5 neighborhoods,  50 locations,  500 machines,  1732 services, 20000 processes and 1 balancePenalties with a search space of  10^53979.
model_b_5  has  6 resources,  5 neighborhoods,  10 locations,  100 machines, 35082 services, 40000 processes and 0 balancePenalties with a search space of  10^80000.
model_b_6  has  6 resources,  5 neighborhoods,  50 locations,  200 machines, 14680 services, 40000 processes and 1 balancePenalties with a search space of  10^92041.
model_b_7  has  6 resources,  5 neighborhoods,  50 locations, 4000 machines, 15050 services, 40000 processes and 1 balancePenalties with a search space of 10^144082.
model_b_8  has  3 resources,  5 neighborhoods,  10 locations,  100 machines, 45030 services, 50000 processes and 0 balancePenalties with a search space of 10^100000.
model_b_9  has  3 resources,  5 neighborhoods, 100 locations, 1000 machines,  4609 services, 50000 processes and 1 balancePenalties with a search space of 10^150000.
model_b_10 has  3 resources,  5 neighborhoods, 100 locations, 5000 machines,  4896 services, 50000 processes and 1 balancePenalties with a search space of 10^184948.

図4.7 ドメインモデル

マシンの再割り当てクラス図

4.14. 配送経路

複数の車両を使用して、各顧客の品物を回収し、倉庫まで運びます。1 つの車両で複数の顧客から品物を回収することはできますが、収容できる容量には限りがあります。

車両経路の図

基本例 (CVRP) のほかに、時間枠の設定が加わった例 (CVRPTW) もあります。

ハード制約:

  • 車両の容量: 車両は、車載容量を超えて品物を運ぶことができない。
  • 時間枠 (CVRPTW のみ):

    • 移動時間: 別の場所に移動する場合には時間がかかる。
    • 顧客対応の時間: 車両は顧客に対応している時間、顧客先にとどまる必要がある。
    • 顧客の準備が整う時間: 顧客の準備が整う前に車両が到着する可能性があるが、準備ができるまで待機してから顧客に対応する必要がある。
    • 顧客が設定した締め切り時間: 車両は、顧客が設定した締め切り時間までに到着する必要がある。

ソフト制約:

  • 合計距離: 車両が移動する合計距離 (ガソリンの消費量) を最小限に抑える。

CVRP (Capacitated Vehicle Routing Problem)と CVRPTW (Capacitated Vehicle Routing Problem)は、Networking and Emerging Optimization (NEO) VRP Web サイトで定義されています。

図4.8 価値提案

車両の配送順序をより効率的にする配送経路の価値提案

問題の規模

CVRP インスタンス (時間枠なし):

belgium-n50-k10             has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-n100-k10            has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-n500-k20            has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-n1000-k20           has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-n2750-k55           has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.
belgium-road-km-n50-k10     has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-road-km-n100-k10    has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-road-km-n500-k20    has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-road-km-n1000-k20   has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-road-km-n2750-k55   has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.
belgium-road-time-n50-k10   has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-road-time-n100-k10  has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-road-time-n500-k20  has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-road-time-n1000-k20 has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-road-time-n2750-k55 has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.
belgium-d2-n50-k10          has  2 depots, 10 vehicles and   48 customers with a search space of   10^74.
belgium-d3-n100-k10         has  3 depots, 10 vehicles and   97 customers with a search space of  10^170.
belgium-d5-n500-k20         has  5 depots, 20 vehicles and  495 customers with a search space of 10^1168.
belgium-d8-n1000-k20        has  8 depots, 20 vehicles and  992 customers with a search space of 10^2607.
belgium-d10-n2750-k55       has 10 depots, 55 vehicles and 2740 customers with a search space of 10^8380.

A-n32-k5  has 1 depots,  5 vehicles and  31 customers with a search space of  10^40.
A-n33-k5  has 1 depots,  5 vehicles and  32 customers with a search space of  10^41.
A-n33-k6  has 1 depots,  6 vehicles and  32 customers with a search space of  10^42.
A-n34-k5  has 1 depots,  5 vehicles and  33 customers with a search space of  10^43.
A-n36-k5  has 1 depots,  5 vehicles and  35 customers with a search space of  10^46.
A-n37-k5  has 1 depots,  5 vehicles and  36 customers with a search space of  10^48.
A-n37-k6  has 1 depots,  6 vehicles and  36 customers with a search space of  10^49.
A-n38-k5  has 1 depots,  5 vehicles and  37 customers with a search space of  10^49.
A-n39-k5  has 1 depots,  5 vehicles and  38 customers with a search space of  10^51.
A-n39-k6  has 1 depots,  6 vehicles and  38 customers with a search space of  10^52.
A-n44-k7  has 1 depots,  7 vehicles and  43 customers with a search space of  10^61.
A-n45-k6  has 1 depots,  6 vehicles and  44 customers with a search space of  10^62.
A-n45-k7  has 1 depots,  7 vehicles and  44 customers with a search space of  10^63.
A-n46-k7  has 1 depots,  7 vehicles and  45 customers with a search space of  10^65.
A-n48-k7  has 1 depots,  7 vehicles and  47 customers with a search space of  10^68.
A-n53-k7  has 1 depots,  7 vehicles and  52 customers with a search space of  10^77.
A-n54-k7  has 1 depots,  7 vehicles and  53 customers with a search space of  10^79.
A-n55-k9  has 1 depots,  9 vehicles and  54 customers with a search space of  10^82.
A-n60-k9  has 1 depots,  9 vehicles and  59 customers with a search space of  10^91.
A-n61-k9  has 1 depots,  9 vehicles and  60 customers with a search space of  10^93.
A-n62-k8  has 1 depots,  8 vehicles and  61 customers with a search space of  10^94.
A-n63-k9  has 1 depots,  9 vehicles and  62 customers with a search space of  10^97.
A-n63-k10 has 1 depots, 10 vehicles and  62 customers with a search space of  10^98.
A-n64-k9  has 1 depots,  9 vehicles and  63 customers with a search space of  10^99.
A-n65-k9  has 1 depots,  9 vehicles and  64 customers with a search space of 10^101.
A-n69-k9  has 1 depots,  9 vehicles and  68 customers with a search space of 10^108.
A-n80-k10 has 1 depots, 10 vehicles and  79 customers with a search space of 10^130.
F-n45-k4  has 1 depots,  4 vehicles and  44 customers with a search space of  10^60.
F-n72-k4  has 1 depots,  4 vehicles and  71 customers with a search space of 10^108.
F-n135-k7 has 1 depots,  7 vehicles and 134 customers with a search space of 10^240.

CVRPTW インスタンス (時間枠あり):

belgium-tw-d2-n50-k10    has  2 depots, 10 vehicles and   48 customers with a search space of   10^74.
belgium-tw-d3-n100-k10   has  3 depots, 10 vehicles and   97 customers with a search space of  10^170.
belgium-tw-d5-n500-k20   has  5 depots, 20 vehicles and  495 customers with a search space of 10^1168.
belgium-tw-d8-n1000-k20  has  8 depots, 20 vehicles and  992 customers with a search space of 10^2607.
belgium-tw-d10-n2750-k55 has 10 depots, 55 vehicles and 2740 customers with a search space of 10^8380.
belgium-tw-n50-k10       has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-tw-n100-k10      has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-tw-n500-k20      has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-tw-n1000-k20     has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-tw-n2750-k55     has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.

Solomon_025_C101       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_C201       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_R101       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_R201       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_RC101      has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_RC201      has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_100_C101       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_C201       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_R101       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_R201       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_RC101      has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_RC201      has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Homberger_0200_C1_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_C2_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_R1_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_R2_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_RC1_2_1 has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_RC2_2_1 has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0400_C1_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_C2_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_R1_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_R2_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_RC1_4_1 has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_RC2_4_1 has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0600_C1_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_C2_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_R1_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_R2_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_RC1_6_1 has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_RC2_6_1 has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0800_C1_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_C2_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_R1_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_R2_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_RC1_8_1 has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_RC2_8_1 has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_1000_C110_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_C210_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_R110_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_R210_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_RC110_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_RC210_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.

4.14.1. 配送経路のドメインモデル

配送経路クラス図

時間枠ありの配送経路のドメインモデルでは、シャドウ変数の機能を多用します。こうすることで、arrivalTimedepartureTime などのプロパティーがドメインモデルで直接利用できるため、制約をより自然に表現できます。

直線距離ではなく道路の距離

車は、直線距離を移動するのではなく、道路や高速道路を使用する必要があります。ビジネスの観点からすると、これは非常に重要です。

配送経路の距離タイプ

最適化アルゴリズムでは、2 点の距離を検索できている (できれば、事前に計算されている) 場合には、これは特に重要ではありません。道路費は距離である必要はありません。また、移動時間、フラッシュコスト、または加重関数を使用することもできます。GraphHopper (埋め込み可能なオフライン Java エンジン)、Open MapQuest (web サービス)、Google Maps Client API (web サービス) など、移動コストを事前に計算する技術があります。

実際の地図との統合

また、LeafletGoogle Maps for developers など、レンダリングする技術も複数あります。

Leaflt.js と Google Maps の比較

GraphHopper または Google Map Directions を使用して実際の経路をレンダリングすることも可能ですが、高速道路で経路が重なるため、停止する順番を確認するのが困難になります。

いくつかのデータポイントを含むベルギーの地図

2 点間の移動費は、OptaPlanner で使用されるのと同じ最適化条件を使用する点に注意してください。たとえば、GraphHopper はデフォルトで、最短ではなく、最速の経路を返します。最速の GPS 経路の km (またはマイル) の距離を使用して、OptaPlanner で最短の移動を最適化しないようにしてください。以下のように、準最適な解が導き出される可能性があります。

道路距離の三角形不等号図

一般的な考え方とは異なり、多くのユーザーは最短の経路ではなく、最速の経路を使用したいと考えます。通常の道路よりも高速道路の使用を好みます。舗装されていない道よりも舗装されている道路を好みます。実際には、最速の経路と、最短の経路が同じであることはほとんどありません。

4.15. プロジェクトジョブのスケジュール

プロジェクトの遅延を最小限に抑えるために、すべてのジョブを時間内に実行できるようにスケジュールを設定します。各ジョブは、プロジェクトに含まれます。ジョブは、異なる方法で実行できます。方法ごとに期間や使用するリソースが異なります。これは、柔軟な ジョブショップスケジューリング (JSP) の応用です。

プロジェクトジョブのスケジュール設定の例

ハード制約:

  • ジョブの優先順位: ジョブは、先行のジョブがすべて完了するまで開始しない。
  • リソースの容量: 利用可能な量を超えるリソースを使用しない。

    • リソースはローカル (同じプロジェクトのジョブ間で共有)、またはグローバル (全ジョブ間で共有) とする。
    • リソースは更新可能 (1 日に利用可能な容量) または更新不可 (全日で利用可能な容量) とする。

中程度の制約:

  • プロジェクトの合計遅延時間: 各プロジェクトの所要時間 (メイクスパン) を最短にする。

ソフト制約:

  • メイクスパン合計: 複数のプロジェクトスケジュールの合計所要時間を最短にする。

この問題は、the MISTA 2013 challenge で定義されています。

問題の規模

Schedule A-1  has  2 projects,  24 jobs,   64 execution modes,  7 resources and  150 resource requirements.
Schedule A-2  has  2 projects,  44 jobs,  124 execution modes,  7 resources and  420 resource requirements.
Schedule A-3  has  2 projects,  64 jobs,  184 execution modes,  7 resources and  630 resource requirements.
Schedule A-4  has  5 projects,  60 jobs,  160 execution modes, 16 resources and  390 resource requirements.
Schedule A-5  has  5 projects, 110 jobs,  310 execution modes, 16 resources and  900 resource requirements.
Schedule A-6  has  5 projects, 160 jobs,  460 execution modes, 16 resources and 1440 resource requirements.
Schedule A-7  has 10 projects, 120 jobs,  320 execution modes, 22 resources and  900 resource requirements.
Schedule A-8  has 10 projects, 220 jobs,  620 execution modes, 22 resources and 1860 resource requirements.
Schedule A-9  has 10 projects, 320 jobs,  920 execution modes, 31 resources and 2880 resource requirements.
Schedule A-10 has 10 projects, 320 jobs,  920 execution modes, 31 resources and 2970 resource requirements.
Schedule B-1  has 10 projects, 120 jobs,  320 execution modes, 31 resources and  900 resource requirements.
Schedule B-2  has 10 projects, 220 jobs,  620 execution modes, 22 resources and 1740 resource requirements.
Schedule B-3  has 10 projects, 320 jobs,  920 execution modes, 31 resources and 3060 resource requirements.
Schedule B-4  has 15 projects, 180 jobs,  480 execution modes, 46 resources and 1530 resource requirements.
Schedule B-5  has 15 projects, 330 jobs,  930 execution modes, 46 resources and 2760 resource requirements.
Schedule B-6  has 15 projects, 480 jobs, 1380 execution modes, 46 resources and 4500 resource requirements.
Schedule B-7  has 20 projects, 240 jobs,  640 execution modes, 61 resources and 1710 resource requirements.
Schedule B-8  has 20 projects, 440 jobs, 1240 execution modes, 42 resources and 3180 resource requirements.
Schedule B-9  has 20 projects, 640 jobs, 1840 execution modes, 61 resources and 5940 resource requirements.
Schedule B-10 has 20 projects, 460 jobs, 1300 execution modes, 42 resources and 4260 resource requirements.

4.16. タスクの割り当て

従業員のキューのスポットに各タスクを割り当てます。タスクごとに、従業員のアフィニティーレベルから影響を受ける期間と、タスクの顧客が含まれます。

ハード制約:

  • スキル: タスクごとに 1 つ以上のスキルが必要である。従業員には、このようなスキルがすべて必要です。

ソフトレベル 0 の制約:

  • 極めて重要なタスク: 主要なタスクやマイナーなタスクの前に、極めて重要なタスクを完了する。

ソフトレベル 1 の制約:

  • メークスパンの最小化: 全タスクを完了するまでの時間を短縮する。

    • 勤務歴の長い従業員から順番に進めていき、公平性やロードバランシングを作成する。

ソフトレベル 2 の制約:

  • 主要なタスク: マイナーなタスクの前に、主要なタスクをできるだけ早く完了する。

ソフトレベル 3 の制約:

  • マイナーなタスク: できるだけ早くマイナーなタスクを完了する。

図4.9 価値提案

タスクの再割り当てと並べ替えによって各従業員のタスクキューを最適化することによるタスク割り当ての図

問題の規模

24tasks-8employees   has  24 tasks, 6 skills,  8 employees,   4 task types and  4 customers with a search space of   10^30.
50tasks-5employees   has  50 tasks, 5 skills,  5 employees,  10 task types and 10 customers with a search space of   10^69.
100tasks-5employees  has 100 tasks, 5 skills,  5 employees,  20 task types and 15 customers with a search space of  10^164.
500tasks-20employees has 500 tasks, 6 skills, 20 employees, 100 task types and 60 customers with a search space of 10^1168.

図4.10 ドメインモデル

タスク割り当てクラス図

4.17. 試験の時間割 (ITC 2007 track 1 - 試験)

すべての試験に、時間と部屋を割り当てます。同じ時間帯に同じ部屋で、複数の試験を行うことができるものとします。

各試験に期間と部屋を割り当てた試験時間割の図

ハード制約:

  • 試験の制約: 同じ学生が受ける 2 つの試験は、同じ時間帯に実施できないものとする。
  • 教室の収容人数: 教室の座席数は、常に受験者数よりも多くなければならない。
  • 期間: 期間は、すべての試験に対応できる長さでなければならない。
  • 期間関連のハード制約 (データセットごとに指定):

    • 一致: 特定の 2 つの試験を同じ時間帯に設定する必要がある (別の教室を使用することも可能)。
    • 除外: 特定の 2 つの試験を同じ時間帯に設定できない。
    • 以降: 特定の試験を、別の特定の試験の後に行う必要がある。
  • 教室関連の制約 (データセットごとに指定):

    • 排他的: 特定の試験を、他の試験と同じ教室で行うことはできない。

ソフト制約 (パラメーター化されたペナルティーがそれぞれ設定されている):

  • 同じ学生が、続けて試験を 2 つ受けてはいけない。
  • 同じ学生が、同じ日に試験を 2 つ受けてはいけない。
  • 時間帯の分散: 同じ学生が受ける 2 つの試験は、時間をある程度あける。
  • 異なる試験の長さ: 教室を共有する 2 つの試験の長さは、同じにする。
  • 前倒し: 規模の大きい試験は、スケジュールを早めに決定する。
  • 期間のペナルティー (データセットごとに指定): 期間によっては、使用されるとペナルティーが発生する。
  • 部屋のペナルティー (データセットごとに指定): 部屋によっては、使用されるとペナルティーが発生する。

実際に大学から取得した大規模な試験データセットを使用します。

この問題は、International Timetabling Competition 2007 track 1 で定義されています。Geoffrey De Smet は、非常に初期バージョンの OptaPlanner で 4 位を終了しました。このコンペティション以降、多くの改良点が加えられています。

問題の規模

exam_comp_set1 has  7883 students,  607 exams, 54 periods,  7 rooms,  12 period constraints and  0 room constraints with a search space of 10^1564.
exam_comp_set2 has 12484 students,  870 exams, 40 periods, 49 rooms,  12 period constraints and  2 room constraints with a search space of 10^2864.
exam_comp_set3 has 16365 students,  934 exams, 36 periods, 48 rooms, 168 period constraints and 15 room constraints with a search space of 10^3023.
exam_comp_set4 has  4421 students,  273 exams, 21 periods,  1 rooms,  40 period constraints and  0 room constraints with a search space of  10^360.
exam_comp_set5 has  8719 students, 1018 exams, 42 periods,  3 rooms,  27 period constraints and  0 room constraints with a search space of 10^2138.
exam_comp_set6 has  7909 students,  242 exams, 16 periods,  8 rooms,  22 period constraints and  0 room constraints with a search space of  10^509.
exam_comp_set7 has 13795 students, 1096 exams, 80 periods, 15 rooms,  28 period constraints and  0 room constraints with a search space of 10^3374.
exam_comp_set8 has  7718 students,  598 exams, 80 periods,  8 rooms,  20 period constraints and  1 room constraints with a search space of 10^1678.

4.17.1. テストの時間割のドメインモデル

以下の図では、主な試験のドメインクラスを紹介しています。

図4.11 試験のドメインクラスの図

ドメインクラスを示す複雑な図

試験のコンセプトを、Exam クラスと Topic クラスに分けた点に注意してください。期間または教室のプロパティーを変更し、解 (プランニングエンティティークラス) を求めると、Exam インスタンスが変化します。このとき、Topic インスタンス、Period インスタンス、および Room インスタンスは変化しません (他のクラスと同様、これらも問題ファクトです)。

4.18. 看護師の勤務表 (INRC 2010)

各シフトに看護師を割り当てます。

病院で看護師をスケジュールする方法を示す従業員シフト名簿の例

ハード制約:

  • 未割り当てのシフトなし (組み込み): すべてのシフトを従業員に割り当てる必要がある。
  • シフトの制約: 従業員には 1 日に 1 シフトだけ割り当てることができる。

ソフト制約:

  • 契約上の義務。この業界では、頻繁に契約上の義務に違反するため、ハード制約ではなく、ソフト制約として定義することに決定しました。

    • 割り当ての下限および上限: 各従業員は、 (それぞれの契約に合わせて) x より多く、y よりも少ないシフト数を勤務する必要がある。
    • 連続勤務日数の下限および上限: 各従業員は、 (それぞれの契約に合わせて) 連続で x 日から y 日間、勤務する必要がある。
    • 連続公休日数の下限および上限: 各従業員は、 (それぞれの契約に合わせて) 連続で x 日から y 日間、休む必要がある。
    • 週末に連続勤務する回数の下限および上限: 各従業員は、 (それぞれの契約に合わせて) 連続で x 回から y 回、週末勤務する必要がある。
    • 週末の勤務有無を同じにする: 各従業員は、週末の両日を勤務する、または休む必要がある。
    • 週末のシフトタイプを同じにする: 各従業員で、同じ週末のシフトタイプは、同じにする必要がある。
    • 好ましくないシフトパターン: 遅番+早番+遅番など、好ましくないシフトタイプを連続で組み合わせたパターン。
  • 従業員の希望:

    • 勤務日のリクエスト: 従業員は、特定の勤務希望日を申請できる。
    • 公休日のリクエスト: 従業員は、特定の公休希望日を申請できる。
    • 勤務するシフトのリクエスト: 従業員は特定のシフトへの割り当てを希望できる。
    • 勤務しないシフトのリクエスト: 従業員は特定のシフトに割り当てられないように希望できる。
  • 他のスキル: スキルに割り当てられた従業員は、そのシフトで必要な全スキルに堪能である必要がある。

この問題は International Nurse Rostering Competition 2010 で定義されています。

図4.12 価値提案

病院で看護師をどのようにスケジュールできるかを示す従業員名簿の図

問題の規模

以下のように、データセットの種類は 3 つあります。

  • sprint: 数秒で問題を解決する必要があります。
  • medium: 数分で問題を解決する必要があります。
  • long: 時間で解決する必要があります。
toy1          has 1 skills, 3 shiftTypes, 2 patterns, 1 contracts,  6 employees,  7 shiftDates,  35 shiftAssignments and   0 requests with a search space of   10^27.
toy2          has 1 skills, 3 shiftTypes, 3 patterns, 2 contracts, 20 employees, 28 shiftDates, 180 shiftAssignments and 140 requests with a search space of  10^234.

sprint01      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint02      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint03      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint04      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint05      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint06      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint07      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint08      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint09      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint10      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_hint01 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_hint02 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_hint03 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late01 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late02 has 1 skills, 3 shiftTypes, 4 patterns, 3 contracts, 10 employees, 28 shiftDates, 144 shiftAssignments and 139 requests with a search space of  10^144.
sprint_late03 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 160 shiftAssignments and 150 requests with a search space of  10^160.
sprint_late04 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 160 shiftAssignments and 150 requests with a search space of  10^160.
sprint_late05 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late06 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late07 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late08 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and   0 requests with a search space of  10^152.
sprint_late09 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and   0 requests with a search space of  10^152.
sprint_late10 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.

medium01      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium02      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium03      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium04      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium05      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium_hint01 has 1 skills, 4 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_hint02 has 1 skills, 4 shiftTypes, 7 patterns, 3 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_hint03 has 1 skills, 4 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_late01 has 1 skills, 4 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 424 shiftAssignments and 390 requests with a search space of  10^626.
medium_late02 has 1 skills, 4 shiftTypes, 7 patterns, 3 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_late03 has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_late04 has 1 skills, 4 shiftTypes, 7 patterns, 3 contracts, 30 employees, 28 shiftDates, 416 shiftAssignments and 390 requests with a search space of  10^614.
medium_late05 has 2 skills, 5 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 452 shiftAssignments and 390 requests with a search space of  10^667.

long01        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long02        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long03        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long04        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long05        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long_hint01   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.
long_hint02   has 2 skills, 5 shiftTypes, 7 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.
long_hint03   has 2 skills, 5 shiftTypes, 7 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.
long_late01   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late02   has 2 skills, 5 shiftTypes, 9 patterns, 4 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late03   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late04   has 2 skills, 5 shiftTypes, 9 patterns, 4 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late05   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.

図4.13 ドメインモデル

看護師名簿クラス図

4.19. 患者の入院スケジュール

患者入院スケジュール (PAS) は、病院のベッド計画とも呼ばれ、病院に入院する各患者にベッドを割り当てます。病床は、患者の予定された滞在期間中、患者に割り当てられます。各病床は病室に属し、各病室は部門に属します。患者の来院日と退院日は決まっています。病床を割り当てるだけで済みます。

この問題は、過度に制約されたデータセットを特徴としています。すべてのプランニングエンティティーを割り当てる必要がない場合は、厳しい制約に違反することなく、必要な数のエンティティーを割り当てることが適切です。これは、過剰制約プランニングと呼ばれます。

患者の入院スケジュール

ハード制約:

  • 同じ夜に 2 人の患者を同じ病床に割り当ててはなりません。重量: -1000hard * conflictNightCount
  • 病室には性別制限を設けることができます。女性のみ、男性のみ、同じ夜に同性が宿泊できる、または性別制限がまったくないなどです。重量: -50hard * nightCount
  • 部門には最低年齢または最高年齢を設定できます。重量: -100hard * nightCount
  • 患者は特定の設備を備えた部屋を要求する場合があります。重量: -50hard * nightCount

中程度の制約:

  • データセットが過度に制約されていない限り、すべての患者をベッドに割り当てます。重量: -1medium * nightCount

ソフト制約:

  • 患者は、たとえば一人部屋を希望する場合など、部屋の最大サイズの好みを指定できます。重量: -8soft * nightCount
  • 患者は、その患者の病状を専門とする部門に割り当てるのが最善です。重量: -10soft * nightCount.
  • 患者は、その患者の病状を専門とする病室に割り当てるのが最善です。重量: -20soft * nightCount

    • 病室の専門性は、優先度 1 である必要があります。重量: -10soft *(priority - 1)* nightCount.
  • 患者は、特定の設備を備えた部屋の希望を指定できます。重量: -20soft * nightCount

問題は Kaho’s Patient Scheduling のバリエーションであり、データセットは実際の病院から取得します。

問題の規模

overconstrained01 has 6 specialisms, 4 equipments, 1 departments,  25 rooms,  69 beds, 14 nights,  519 patients and  519 admissions with a search space of 10^958.
testdata01        has 4 specialisms, 2 equipments, 4 departments,  98 rooms, 286 beds, 14 nights,  652 patients and  652 admissions with a search space of 10^1603.
testdata02        has 6 specialisms, 2 equipments, 6 departments, 151 rooms, 465 beds, 14 nights,  755 patients and  755 admissions with a search space of 10^2015.
testdata03        has 5 specialisms, 2 equipments, 5 departments, 131 rooms, 395 beds, 14 nights,  708 patients and  708 admissions with a search space of 10^1840.
testdata04        has 6 specialisms, 2 equipments, 6 departments, 155 rooms, 471 beds, 14 nights,  746 patients and  746 admissions with a search space of 10^1995.
testdata05        has 4 specialisms, 2 equipments, 4 departments, 102 rooms, 325 beds, 14 nights,  587 patients and  587 admissions with a search space of 10^1476.
testdata06        has 4 specialisms, 2 equipments, 4 departments, 104 rooms, 313 beds, 14 nights,  685 patients and  685 admissions with a search space of 10^1711.
testdata07        has 6 specialisms, 4 equipments, 6 departments, 162 rooms, 472 beds, 14 nights,  519 patients and  519 admissions with a search space of 10^1389.
testdata08        has 6 specialisms, 4 equipments, 6 departments, 148 rooms, 441 beds, 21 nights,  895 patients and  895 admissions with a search space of 10^2368.
testdata09        has 4 specialisms, 4 equipments, 4 departments, 105 rooms, 310 beds, 28 nights, 1400 patients and 1400 admissions with a search space of 10^3490.
testdata10        has 4 specialisms, 4 equipments, 4 departments, 104 rooms, 308 beds, 56 nights, 1575 patients and 1575 admissions with a search space of 10^3922.
testdata11        has 4 specialisms, 4 equipments, 4 departments, 107 rooms, 318 beds, 91 nights, 2514 patients and 2514 admissions with a search space of 10^6295.
testdata12        has 4 specialisms, 4 equipments, 4 departments, 105 rooms, 310 beds, 84 nights, 2750 patients and 2750 admissions with a search space of 10^6856.
testdata13        has 5 specialisms, 4 equipments, 5 departments, 125 rooms, 368 beds, 28 nights,  907 patients and 1109 admissions with a search space of 10^2847.

図4.14 ドメインモデル

病床割り当てクラス図

4.20. 巡回トーナメント問題 (TTP)

n 人数のチーム間の一致をスケジュールします。

巡回トーナメント問題

ハード制約:

  • 各チームは、他のチームとそれぞれ 2 回 (ホームとアウェイ) 試合をする。
  • 各チームは、各時間枠に 1 試合だけ行う。
  • 3 回連続で、ホームまたはアウェイでの試合はできない。
  • 繰り返しなし: 同じ対戦相手と 2 回連続で対戦できない。

ソフト制約:

  • 全チームが移動する合計距離を最小限に抑える。

この問題は Michael Trick の Web サイト (世界記録が含まれます) で定義されています。

問題の規模

1-nl04     has  6 days,  4 teams and   12 matches with a search space of    10^5.
1-nl06     has 10 days,  6 teams and   30 matches with a search space of   10^19.
1-nl08     has 14 days,  8 teams and   56 matches with a search space of   10^43.
1-nl10     has 18 days, 10 teams and   90 matches with a search space of   10^79.
1-nl12     has 22 days, 12 teams and  132 matches with a search space of  10^126.
1-nl14     has 26 days, 14 teams and  182 matches with a search space of  10^186.
1-nl16     has 30 days, 16 teams and  240 matches with a search space of  10^259.
2-bra24    has 46 days, 24 teams and  552 matches with a search space of  10^692.
3-nfl16    has 30 days, 16 teams and  240 matches with a search space of  10^259.
3-nfl18    has 34 days, 18 teams and  306 matches with a search space of  10^346.
3-nfl20    has 38 days, 20 teams and  380 matches with a search space of  10^447.
3-nfl22    has 42 days, 22 teams and  462 matches with a search space of  10^562.
3-nfl24    has 46 days, 24 teams and  552 matches with a search space of  10^692.
3-nfl26    has 50 days, 26 teams and  650 matches with a search space of  10^838.
3-nfl28    has 54 days, 28 teams and  756 matches with a search space of  10^999.
3-nfl30    has 58 days, 30 teams and  870 matches with a search space of 10^1175.
3-nfl32    has 62 days, 32 teams and  992 matches with a search space of 10^1367.
4-super04  has  6 days,  4 teams and   12 matches with a search space of    10^5.
4-super06  has 10 days,  6 teams and   30 matches with a search space of   10^19.
4-super08  has 14 days,  8 teams and   56 matches with a search space of   10^43.
4-super10  has 18 days, 10 teams and   90 matches with a search space of   10^79.
4-super12  has 22 days, 12 teams and  132 matches with a search space of  10^126.
4-super14  has 26 days, 14 teams and  182 matches with a search space of  10^186.
5-galaxy04 has  6 days,  4 teams and   12 matches with a search space of    10^5.
5-galaxy06 has 10 days,  6 teams and   30 matches with a search space of   10^19.
5-galaxy08 has 14 days,  8 teams and   56 matches with a search space of   10^43.
5-galaxy10 has 18 days, 10 teams and   90 matches with a search space of   10^79.
5-galaxy12 has 22 days, 12 teams and  132 matches with a search space of  10^126.
5-galaxy14 has 26 days, 14 teams and  182 matches with a search space of  10^186.
5-galaxy16 has 30 days, 16 teams and  240 matches with a search space of  10^259.
5-galaxy18 has 34 days, 18 teams and  306 matches with a search space of  10^346.
5-galaxy20 has 38 days, 20 teams and  380 matches with a search space of  10^447.
5-galaxy22 has 42 days, 22 teams and  462 matches with a search space of  10^562.
5-galaxy24 has 46 days, 24 teams and  552 matches with a search space of  10^692.
5-galaxy26 has 50 days, 26 teams and  650 matches with a search space of  10^838.
5-galaxy28 has 54 days, 28 teams and  756 matches with a search space of  10^999.
5-galaxy30 has 58 days, 30 teams and  870 matches with a search space of 10^1175.
5-galaxy32 has 62 days, 32 teams and  992 matches with a search space of 10^1367.
5-galaxy34 has 66 days, 34 teams and 1122 matches with a search space of 10^1576.
5-galaxy36 has 70 days, 36 teams and 1260 matches with a search space of 10^1801.
5-galaxy38 has 74 days, 38 teams and 1406 matches with a search space of 10^2042.
5-galaxy40 has 78 days, 40 teams and 1560 matches with a search space of 10^2301.

4.21. コストを抑えるスケジュール

全タスクを時間内にスケジュールし、機械の電気代を最小限に抑えます。電気代は時間によって異なります。これは、ジョブショップスケジューリング の応用です。

ハード制約:

  • 開始時間の制限: 各タスクは、最早と最遅の開始時間の制限内に、開始する必要がある。
  • 最大容量: マシンに割り当てる各リソースはこの量を超えてはいけない。
  • 開始および終了: 各機械は、タスクが割り当てられている間は稼働している必要がある。次のタスクまでの間、起動および終了コストを避けるため、機械をアイドルにすることができる。

中程度の制約:

  • 電気代: 全スケジュールの合計電気代を最小限に抑える。

    • 機械の電気代: 稼働中またはアイドル中の機械はそれぞれ、電気を消費し、電気代が発生する (金額は使用時の電気代によって異なる)。
    • タスクの電気代: 各タスクも電気を消費し、電気代が発生する (金額は使用時の電気代によって異なる)。
    • 機械の起動および終了コスト: 機械を起動または終了するたびに、追加のコストが発生する。

ソフト制約 (問題に元々設定されている定義に追加):

  • 早く開始: なるべく早めにタスクを開始するようにする。

この問題は、ICON challenge で定義されています。

問題の規模

sample01   has 3 resources,   2 machines, 288 periods and   25 tasks with a search space of    10^53.
sample02   has 3 resources,   2 machines, 288 periods and   50 tasks with a search space of   10^114.
sample03   has 3 resources,   2 machines, 288 periods and  100 tasks with a search space of   10^226.
sample04   has 3 resources,   5 machines, 288 periods and  100 tasks with a search space of   10^266.
sample05   has 3 resources,   2 machines, 288 periods and  250 tasks with a search space of   10^584.
sample06   has 3 resources,   5 machines, 288 periods and  250 tasks with a search space of   10^673.
sample07   has 3 resources,   2 machines, 288 periods and 1000 tasks with a search space of  10^2388.
sample08   has 3 resources,   5 machines, 288 periods and 1000 tasks with a search space of  10^2748.
sample09   has 4 resources,  20 machines, 288 periods and 2000 tasks with a search space of  10^6668.
instance00 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^595.
instance01 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^599.
instance02 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^599.
instance03 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^591.
instance04 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^590.
instance05 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^667.
instance06 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^660.
instance07 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^662.
instance08 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^651.
instance09 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^659.
instance10 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1657.
instance11 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1644.
instance12 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1637.
instance13 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1659.
instance14 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1643.
instance15 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1782.
instance16 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1778.
instance17 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1764.
instance18 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1769.
instance19 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1778.
instance20 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3689.
instance21 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3678.
instance22 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3706.
instance23 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3676.
instance24 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3681.
instance25 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3774.
instance26 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3737.
instance27 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3744.
instance28 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3731.
instance29 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3746.
instance30 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7718.
instance31 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7740.
instance32 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7686.
instance33 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7672.
instance34 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7695.
instance35 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7807.
instance36 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7814.
instance37 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7764.
instance38 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7736.
instance39 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7783.
instance40 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15976.
instance41 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15935.
instance42 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15887.
instance43 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15896.
instance44 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15885.
instance45 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20173.
instance46 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20132.
instance47 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20126.
instance48 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20110.
instance49 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20078.

4.22. 投資資産クラスの割り当て (ポートフォリオの最適化)

各資産クラスに投資する相対数を決定します。

ハード制約:

  • リスクの最大値: 標準偏差合計は、標準偏差の最大値を超えてはならない。

    • 標準偏差合計の計算は、Markowitz Portfolio Theory を適用した、資産クラスの相対関係を考慮する必要がある。
  • 地域の最大値: 地域ごとに数量の最大値がある。
  • セクターの最大値: 各セクターに数量の最大値がある。

ソフト制約:

  • 期待収益を最大化する。

問題の規模

de_smet_1 has 1 regions, 3 sectors and 11 asset classes with a search space of 10^4.
irrinki_1 has 2 regions, 3 sectors and 6 asset classes with a search space of 10^3.

サイズが大きいデータセットは作成/検証されていませんが、問題はないはずです。データに関する適切な情報源として、このアセット相関の Web サイト を参照してください。

4.23. 会議スケジュール

各会議を時間帯と部屋に割り当てていきます。時間帯は重複させることができます。LibreOffice や Excel で編集可能な *.xlsx ファイルとの読み書きが可能です。

ハード制約:

  • 時間帯の会議タイプ: 会議のタイプは、時間帯の会議タイプと一致する必要がある。
  • 部屋が使用中の時間帯: その会議の時間帯に、会議用の部屋が利用できなければならない。
  • 部屋の競合: 2 つの会議が、同じ時間に同じ会議室を使用することはできない。
  • 講演者が空いていない時間帯: 講演者は必ず、会議の時間帯に空いていなければならない。
  • 講演者の競合: 同じ時間帯の 2 つの会議に同じ講演者を割り当てることができない。
  • 汎用の時間帯および部屋タグ:

    • 講演者が要求する時間帯タグ: 講演者に、必須時間帯タグが付けられている場合、講演者の会議はそのタグが付いている時間に割り当てる必要がある。
    • 講演者の禁止時間帯タグ: 公演者に、禁止時間帯タグが割り当てられている場合は、そのタグの付いた時間帯に講演者の会議をどれも割り当てることができない。
    • 会議を設定する必要のある時間帯タグ: 会議に必須時間帯タグが付いている場合は、そのタグの付いた時間帯に割り当てる必要がある。
    • 会議の禁止時間帯タグ: 会議に、禁止時間帯タグが割り当てられている場合は、そのタグの付いた時間帯にその会議を割り当てることができない。
    • 講演者が要求する部屋のタグ: 講演者に、必須の部屋タグが付けられている場合、講演者の会議はそのタグが付いている部屋に割り当てる必要がある。
    • 講演者が禁止する部屋のタグ: 講演者に、禁止部屋のタグが付けられている場合、講演者の会議はそのタグが付いている部屋に割り当てことができない。
    • 会議を設定する必要のある部屋タグ: 会議に必須部屋タグが付いている場合は、そのタグの付いた部屋に割り当てる必要がある。
    • 会議の禁止部屋タグ: 会議に、禁止部屋タグが割り当てられている場合は、そのタグの付いた部屋にその会議を割り当てることができない。
  • 他の会議と同じ時間帯に設定しないタグ: このタグが付いている会議は、同じ時間帯に重複してスケジュールしてはいけない。
  • 受講条件が付いた会議: 受講条件が付いた会議をすべて完了してからでないと対象の会議をスケジュールしてはいけない。

ソフト制約:

  • テーマの追跡競合: 同じ時間帯で、テーマのタグが付いた会議の数を最小限に抑える。
  • セクターの競合: 同じ時間帯で同じセクタータグの付いた会議の数を最小限に抑える。
  • コンテンツの受講者レベルのフロー違反: すべてのコンテンツタグに対して、上級者用の会議の前に入門レベルの会議をスケジュールする。
  • 受講者レベルの多様性: すべての時間帯において、異なる受講者レベルの会議数を最大限に増やす。
  • 言語の多様性: すべての時間帯において、異なる言語の会議数を最大限を増やす。
  • 汎用の時間帯および部屋タグ:

    • 講演者が希望する時間帯タグ: 講演者に、希望の時間帯タグが付けられている場合、講演者の会議はそのタグが付いている時間に割り当てるようにする。
    • 講演者が希望しないタイムスロットタグ: 講演者が望ましくないタイムスロットタグを持っている場合、そのタグが付いているタイムスロットには講演を割り当ててはいけません。
    • 会議の希望の時間帯タグ: 会議に希望の時間帯タグが付いている場合は、そのタグの付いた時間帯に割り当てるようにする。
    • 会議の設定を希望しない時間帯タグ: 会議に、希望しない時間帯タグが付いている場合は、そのタグの付いた時間帯に割り当てないようにする。
    • 講演者が希望する部屋のタグ: 講演者に、希望の部屋タグが付けられている場合、講演者の会議はそのタグが付いている部屋に割り当てるようにする。
    • 講演者が希望しない部屋のタグ: 講演者に、希望しない部屋タグが付けられている場合、講演者の会議はそのタグが付いている部屋に割り当てるようにする。
    • 会議を希望の部屋タグ: 会議に希望の部屋タグが付いている場合は、そのタグの付いた部屋に割り当てるようにする。
    • 会議での使用を希望しない部屋タグ: 会議に、希望しない部屋タグが付いている場合、そのタグの付いた部屋に割り当てないようにする。
  • 同じ日の会議: テーマタグまたはコンテンツタグを共有する会議は、最低限の日数 (理想的には同じ日) にスケジュールする必要がある。

図4.15 価値提案

各講演を部屋とタイムスロットに割り当てるときに出席者のエクスペリエンスを最適化する会議スケジュールの例

問題の規模

18talks-6timeslots-5rooms    has  18 talks,  6 timeslots and  5 rooms with a search space of  10^26.
36talks-12timeslots-5rooms   has  36 talks, 12 timeslots and  5 rooms with a search space of  10^64.
72talks-12timeslots-10rooms  has  72 talks, 12 timeslots and 10 rooms with a search space of 10^149.
108talks-18timeslots-10rooms has 108 talks, 18 timeslots and 10 rooms with a search space of 10^243.
216talks-18timeslots-20rooms has 216 talks, 18 timeslots and 20 rooms with a search space of 10^552.

4.24. ロックツアー

次のショーへの移動はロックバンクバスを使用し、空いている日のみショーをスケジュールする。

ハード制約:

  • 必要とされるショーをすべてスケジュールする。
  • できるだけ多くのショーをスケジュールする。

中程度の制約:

  • 収益の機会を最大化する。
  • 運転時間を最小限に抑える。
  • できるだけ早く到着する。

ソフト制約:

  • 長時間の運転は避ける。

問題の規模

47shows has 47 shows with a search space of 10^59.

4.25. 航空機乗組員のスケジューリング

パイロットと客室乗務員にフライトを割り当てます。

ハード制約:

  • 必須スキル: フライトの割り当てにはそれぞれ、必要とされるスキルがあります。たとえば、フライト AB0001 ではパイロット 2 名と、客室乗務員 3 名が必要です。
  • フライトの競合: 各従業員は同じ時間に出勤できるフライトは 1 つだけにする。
  • 2 つのフライト間での移動: 2 つのフライトの間で、従業員は到着先の空港と、出発元の空港に移動できる必要がある。たとえば、アンは 10 時にブリュッセルに到着し、15 時にアムステルダムを出発するなどです。
  • 従業員の勤務できない日: 従業員はフライトの当日は空いていなければならない。たとえば、アンは 2 月 1 日に休暇を取っているなど。

ソフト制約:

  • 最初の仕事が自宅から出発する。
  • 最後の仕事が自宅に到着する。
  • 総フライト時間を従業員別に平均的に分散する。

問題の規模

175flights-7days-Europe  has 2 skills, 50 airports, 150 employees, 175 flights and  875 flight assignments with a search space of  10^1904.
700flights-28days-Europe has 2 skills, 50 airports, 150 employees, 700 flights and 3500 flight assignments with a search space of  10^7616.
875flights-7days-Europe  has 2 skills, 50 airports, 750 employees, 875 flights and 4375 flight assignments with a search space of 10^12578.
175flights-7days-US      has 2 skills, 48 airports, 150 employees, 175 flights and  875 flight assignments with a search space of  10^1904.

第5章 Red Hat build of OptaPlanner サンプルのダウンロードおよびビルド

Red Hat Build of OptaPlanner の例は、Red Hat カスタマーポータルで入手できる Red Hat build of OptaPlanner ソースパッケージの一部としてダウンロードできます。

注記

Red Hat build of OptaPlanner は GUI に依存しません。デスクトップと同じように、サーバーまたはモバイル JVM 上でも実行できます。

手順

  1. Red Hat カスタマーポータルの Software Downloads ページに移動し (ログインが必要)、ドロップダウンオプションから製品およびバージョンを選択します。

    • 製品: Red Hat build of OptaPlanner
    • バージョン: 8.33
  2. Red Hat build of OptaPlanner 8.33 Source Distribution をダウンロードします。
  3. rhbop-8.33.0-optaplanner-sources.zip ファイルを展開します。

    展開した org.optaplanner.optaplanner-8.33.0.Final-redhat-00004/optaplanner-examples/src/main/java/org/optaplanner/examples ディレクトリーには、サンプルソースコードが含まれています。

  4. サンプルをビルドするには、org.optaplanner.optaplanner-8.33.0.Final-redhat-00004 ディレクトリーで次のコマンドを入力します。

    mvn clean install -Dquickly
  5. サンプルディレクトリーに移動します。

    optaplanner-examples
  6. 例を実行するには、次のコマンドを入力します。

    mvn exec java

第6章 Red Hat build of Quarkus プラットフォームでの Red Hat build of OptaPlanner の使用

Red Hat build of OptaPlanner は、Red Hat build of Quarkus プラットフォームと統合されます。OptaPlanner の依存関係を含むプラットフォームアーティファクトの依存関係のバージョンは、Quarkus bill of materials (BOM)ファイル com.redhat.quarkus.platform:quarkus-bom で維持されます。どの依存関係バージョンが連携するかを指定する必要はありません。代わりに、Quarkus BOM ファイルを pom.xml 設定ファイルにインポートできます。依存関係のバージョンは <dependencyManagement> セクションに含まれています。そのため、pom.xml ファイルの指定の BOM で管理される個別の Quarkus 依存関係のバージョンを記述する必要はありません。

関連情報

6.1. Apache Maven および Red Hat build of Quarkus

Apache Maven は分散型構築自動化ツールで、ソフトウェアプロジェクトの作成、ビルド、および管理を行うために Java アプリケーション開発で使用されます。Maven は Project Object Model (POM) ファイルと呼ばれる標準の設定ファイルを使用して、プロジェクトの定義や構築プロセスの管理を行います。POM ファイルは、モジュールおよびコンポーネントの依存関係、ビルドの順番、結果となるプロジェクトパッケージのターゲットを記述し、XML ファイルを使用して出力します。これにより、プロジェクトが適切かつ統一された状態でビルドされるようになります。

Maven リポジトリー

Maven リポジトリーには、Java ライブラリー、プラグイン、およびその他のビルドアーティファクトが格納されます。デフォルトのパブリックリポジトリーは Maven 2 Central Repository ですが、複数の開発チームの間で共通のアーティファクトを共有する目的で、社内のプライベートおよび内部リポジトリーとすることが可能です。また、サードパーティーのリポジトリーも利用できます。

Quarkus プロジェクトでオンライン Maven リポジトリーを使用するか、Red Hat build of Quarkus の Maven リポジトリーをダウンロードできます。

Maven プラグイン

Maven プラグインは、POM ファイルの定義済みの部分で 1 つ以上のゴールを達成します。Quarkus アプリケーションは以下の Maven プラグインを使用します。

  • Quarkus Maven プラグイン (quarkus-maven-plugin): Maven による Quarkus プロジェクトの作成を実現して、uber-JAR ファイルの生成をサポートし、開発モードを提供します。
  • Maven Surefire プラグイン (maven-surefire-plugin): ビルドライフサイクルのテストフェーズで使用され、アプリケーションでユニットテストを実行します。プラグインは、テストレポートが含まれるテキストファイルと XML ファイルを生成します。

6.1.1. オンラインリポジトリーの Maven の settings.xml ファイルの設定

ユーザーの settings.xml ファイルを設定して、Maven プロジェクトでオンライン Maven リポジトリーを使用できます。これは、推奨の手法です。リポジトリーマネージャーまたは共有サーバー上のリポジトリーと使用する Maven 設定は、プロジェクトの制御および管理性を向上させます。

注記

Maven の settings.xml ファイルを変更してリポジトリーを設定する場合、変更はすべての Maven プロジェクトに適用されます。

手順

  1. テキストエディターまたは統合開発環境 (IDE) で Maven の ~/.m2/settings.xml ファイルを開きます。

    注記

    ~/.m2/ ディレクトリーに settings.xml ファイルがない場合には、$MAVEN_HOME/.m2/conf/ ディレクトリーから ~/.m2/ ディレクトリーに settings.xml ファイルをコピーします。

  2. 以下の行を Maven の settings.xml ファイルの <profiles> 要素に追加します。

    <!-- Configure the Maven repository -->
    <profile>
      <id>red-hat-enterprise-maven-repository</id>
      <repositories>
        <repository>
          <id>red-hat-enterprise-maven-repository</id>
          <url>https://maven.repository.redhat.com/ga/</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>red-hat-enterprise-maven-repository</id>
          <url>https://maven.repository.redhat.com/ga/</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  3. 以下の行を settings.xml ファイルの <activeProfiles> 要素に追加し、ファイルを保存します。

    <activeProfile>red-hat-enterprise-maven-repository</activeProfile>

6.1.2. Quarkus Maven リポジトリーのダウンロードおよび設定

オンライン Maven リポジトリーを使用しない場合は、Quarkus Maven リポジトリーをダウンロードして設定できます。Quarkus Maven リポジトリーには、Java 開発者がアプリケーションの構築に使用する要件の多くが含まれています。この手順では、Maven の settings.xml ファイルを編集し、Quarkus Maven リポジトリーを設定する方法を説明します。

注記

Maven の settings.xml ファイルを変更してリポジトリーを設定する場合、変更はすべての Maven プロジェクトに適用されます。

手順

  1. Red Hat カスタマーポータルの Software Downloads ページから Red Hat build of Quarkus Maven リポジトリーの ZIP ファイルをダウンロードします。
  2. ダウンロードしたアーカイブを展開します。
  3. ~/.m2/ ディレクトリーに移動し、テキストエディターまたは統合開発環境 (IDE) で Maven の settings.xml ファイルを開きます。
  4. 以下の行を settings.xml ファイルの <profiles> 要素に追加します。ここで、QUARKUS_MAVEN_REPOSITORY はダウンロードした Maven リポジトリーのパスです。QUARKUS_MAVEN_REPOSITORY must be file://$PATH でなければなりません。たとえば file:///home/userX/rh-quarkus-2.13.7.GA-maven-repository/maven-repository のようになります。

    <!-- Configure the Quarkus Maven repository -->
    <profile>
      <id>red-hat-quarkus-maven-repository</id>
      <repositories>
        <repository>
          <id>red-hat-quarkus-maven-repository</id>
          <url>QUARKUS_MAVEN_REPOSITORY</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>red-hat-quarkus-maven-repository</id>
          <url>QUARKUS_MAVEN_REPOSITORY</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  5. 以下の行を settings.xml ファイルの <activeProfiles> 要素に追加し、ファイルを保存します。

    <activeProfile>red-hat-quarkus-maven-repository</activeProfile>
重要

Maven リポジトリーに古いアーティファクトが含まれる場合は、プロジェクトをビルドまたはデプロイしたときに以下のいずれかの Maven エラーメッセージが表示されることがあります。ここで、ARTIFACT_NAME は不明なアーティファクトの名前で、PROJECT_NAME は構築を試みているプロジェクトの名前になります。

  • Missing artifact PROJECT_NAME
  • [ERROR] Failed to execute goal on project ARTIFACT_NAME; Could not resolve dependencies for PROJECT_NAME

この問題を解決するには、~/.m2/repository ディレクトリーにあるローカルリポジトリーのキャッシュバージョンを削除し、最新の Maven アーティファクトを強制的にダウンロードします。

6.2. Maven プラグインを使用した Quarkus プラットフォームでの Red Hat build of OptaPlanner プロジェクトの作成

Apache Maven および Quarkus Maven プラグインを使用して、Red Hat build of OptaPlanner および Quarkus アプリケーションの使用を開始できます。

前提条件

  • OpenJDK 11 以降がインストールされている。Red Hat ビルドの Open JDK は Red Hat カスマーポータル (ログインが必要) の ソフトウェアダウンロード ページから入手できます。
  • Apache Maven 3.8 以降がインストールされている。Maven は Apache Maven Project の Web サイトから入手できます。

手順

  1. コマンドターミナルで以下のコマンドを入力し、Maven が JDK 11 を使用していること、そして Maven のバージョンが 3.8 以上であることを確認します。

    mvn --version
  2. 上記のコマンドで JDK 11 が返されない場合は、JDK 11 へのパスを PATH 環境変数に追加し、上記のコマンドを再度入力します。
  3. Quarkus OptaPlanner クイックスタートプロジェクトを生成するには、以下のコマンドを入力します。ここで、redhat-0000x は Quarkus BOM ファイルの現在のバージョンに置き換えます。

    mvn com.redhat.quarkus.platform:quarkus-maven-plugin:2.13.7.SP1-redhat-0000x:create \
        -DprojectGroupId=com.example \
        -DprojectArtifactId=optaplanner-quickstart  \
        -DplatformGroupId=com.redhat.quarkus.platform
        -DplatformArtifactId=quarkus-bom
        -DplatformVersion=2.13.7.SP1-redhat-0000x \
        -DnoExamples
        -Dextensions="resteasy,resteasy-jackson,optaplanner-quarkus,optaplanner-quarkus-jackson" \

    このコマンドは、./optaplanner-quickstart ディレクトリーで以下の要素を作成します。

    • Maven の構造
    • src/main/dockerDockerfile ファイルの例
    • アプリケーションの設定ファイル

      表6.1 mvn io.quarkus:quarkus-maven-plugin:2.13.7.SP1-redhat-0000x:create コマンドで使用されるプロパティー
      プロパティー説明

      projectGroupId

      プロジェクトのグループ ID。

      projectArtifactId

      プロジェクトのアーティファクト ID。

      extensions

      このプロジェクトで使用する Quarkus 拡張のコンマ区切りリスト。Quarkus 拡張の全一覧については、特定のコマンドラインで mvn quarkus:list-extensions を入力します。

      noExamples

      テストまたはクラスを使用せずに、プロジェクト構造でプロジェクトを作成します。

      projectGroupID および projectArtifactID プロパティーの値を使用して、プロジェクトバージョンを生成します。デフォルトのプロジェクトバージョンは 1.0.0-SNAPSHOT です。

  4. OptaPlanner プロジェクトを表示するには、OptaPlanner Quickstarts ディレクトリーに移動します。

    cd optaplanner-quickstart
  5. pom.xml ファイルを確認します。コンテンツの例を以下に示します。

    <?xml version="1.0"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.example</groupId>
      <artifactId>optaplanner-quickstart</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      <properties>
        <compiler-plugin.version>3.8.1</compiler-plugin.version>
        <maven.compiler.release>11</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>com.redhat.quarkus.platform</quarkus.platform.group-id>
        <quarkus.platform.version>2.13.7.SP1-redhat-0000x</quarkus.platform.version>
        <skipITs>true</skipITs>
        <surefire-plugin.version>3.0.0-M7</surefire-plugin.version>
      </properties>
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>${quarkus.platform.group-id}</groupId>
            <artifactId>${quarkus.platform.artifact-id}</artifactId>
            <version>${quarkus.platform.version}</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
          <dependency>
            <groupId>${quarkus.platform.group-id}</groupId>
            <artifactId>quarkus-optaplanner-bom</artifactId>
            <version>${quarkus.platform.version}</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
        </dependencies>
      </dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.optaplanner</groupId>
          <artifactId>optaplanner-quarkus</artifactId>
        </dependency>
        <dependency>
          <groupId>org.optaplanner</groupId>
          <artifactId>optaplanner-quarkus-jackson</artifactId>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-resteasy-jackson</artifactId>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-resteasy</artifactId>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-arc</artifactId>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-junit5</artifactId>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>io.rest-assured</groupId>
          <artifactId>rest-assured</artifactId>
          <scope>test</scope>
        </dependency>
      </dependencies>
      <repositories>
        <repository>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
          <id>redhat</id>
          <url>https://maven.repository.redhat.com/ga</url>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
          <id>redhat</id>
          <url>https://maven.repository.redhat.com/ga</url>
        </pluginRepository>
      </pluginRepositories>
      <build>
        <plugins>
          <plugin>
            <groupId>${quarkus.platform.group-id}</groupId>
            <artifactId>quarkus-maven-plugin</artifactId>
            <version>${quarkus.platform.version}</version>
            <extensions>true</extensions>
            <executions>
              <execution>
                <goals>
                  <goal>build</goal>
                  <goal>generate-code</goal>
                  <goal>generate-code-tests</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${compiler-plugin.version}</version>
            <configuration>
              <compilerArgs>
                <arg>-parameters</arg>
              </compilerArgs>
            </configuration>
          </plugin>
          <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${surefire-plugin.version}</version>
            <configuration>
              <systemPropertyVariables>
                <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                <maven.home>${maven.home}</maven.home>
              </systemPropertyVariables>
            </configuration>
          </plugin>
          <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>${surefire-plugin.version}</version>
            <executions>
              <execution>
                <goals>
                  <goal>integration-test</goal>
                  <goal>verify</goal>
                </goals>
                <configuration>
                  <systemPropertyVariables>
                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    <maven.home>${maven.home}</maven.home>
                  </systemPropertyVariables>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
      <profiles>
        <profile>
          <id>native</id>
          <activation>
            <property>
              <name>native</name>
            </property>
          </activation>
          <properties>
            <skipITs>false</skipITs>
            <quarkus.package.type>native</quarkus.package.type>
          </properties>
        </profile>
      </profiles>
    </project>

6.3. code.quarkus.redhat.com を使用した Quarkus プラットフォームでの Red Hat build of OptaPlanner プロジェクトの作成

https://code.quarkus.redhat.com の Web サイトを使用して Red Hat build of OptaPlanner Quarkus の Maven プロジェクトを生成し、アプリケーションで使用する拡張機能を自動的に追加および設定できます。

本セクションでは、以下のトピックを含む OptaPlanner Maven プロジェクトを生成するプロセスについて説明します。

  • アプリケーションの基本情報を指定する
  • プロジェクトに追加する拡張を選択する
  • プロジェクトファイルでダウンロード可能なアーカイブを生成する
  • アプリケーションのコンパイルおよび起動のカスタムコマンドを使用する

前提条件

  • Web ブラウザーがある。

手順

  1. Web ブラウザーで https://code.quarkus.redhat.com を開きます。
  2. プロジェクトの詳細を指定します。
  3. プロジェクトのグループ名を入力します。名前の形式は、Java パッケージ命名規則に従います (例: com.example)。
  4. プロジェクトから生成された Maven アーティファクトに使用する名前を入力します (例: code-with-quarkus)。
  5. Build Tool > Maven を選択して、作成するプロジェクトが Maven プロジェクトであることを指定します。選択するビルドツールにより、以下の項目が決定されます。

    • 生成されたプロジェクトのディレクトリー構造。
    • 生成されたプロジェクトで使用される設定ファイルの形式。
    • アプリケーションのコンパイルおよび起動用のカスタムビルドスクリプトおよびコマンド (プロジェクトの生成後に code.quarkus.redhat.com が表示)。

      注記

      Red Hat は、code.quarkus.redhat.com を使用した OptaPlanner Maven プロジェクトの作成だけをサポートします。Red Hat では、Gradle プロジェクトの生成はサポートしていません。

  6. プロジェクトから生成されたアーティファクトで使用するバージョンを入力します。このフィールドのデフォルト値は 1.0.0-SNAPSHOT です。semantic versioning の使用が推奨されますが、必要に応じて、別のタイプのバージョンを使用できます。
  7. プロジェクトをパッケージ化する時に、ビルドツールが生成するアーティファクトのパッケージ名を入力します。

    パッケージ名は、Java パッケージの命名規則に従い、プロジェクトに使用するグループ名と一致するはずですが、別の名前を指定することもできます。

  8. 以下の拡張を選択して、依存関係として組み込みます。

    • RESTEasy JAX-RS (quarkus-resteasy)
    • RESTEasy Jackson (quarkus-resteasy-jackson)
    • OptaPlanner AI 制約ソルバー (optaplanner-quarkus)
    • OptaPlanner Jackson (optaplanner-quarkus-jackson)

      Red Hat は、一覧にある個別の拡張に対してさまざまなレベルのサポートを提供します。レベルは、各拡張名の横にあるラベルで示されています。

      • SUPPORTED 拡張: Red Hat は、実稼働環境のエンタープライズアプリケーションでの使用を完全にサポートします。
      • TECH-PREVIEW 拡張: Red Hat は、テクノロジープレビュー機能のサポート範囲 に基づき、限定的に、実稼働環境でのサポートを提供します。
      • DEV-SUPPORT 拡張: Red Hat は、実稼働環境での使用をサポートしていません。ただし、新規アプリケーションの開発での使用に対しては、Red Hat 開発者がこれらのコア機能をサポートしています。
      • DEPRECATED 拡張: 同じ機能を提供する新しいテクノロジーまたは実装に置き換える予定です。

        Red Hat では、ラベル付けされていない拡張の実稼働環境での使用はサポートしていません。

  9. Generate your application を選択して選択内容を確認し、生成されたプロジェクトを含むアーカイブのダウンロードリンクのオーバーレイ画面を表示します。オーバーレイ画面には、アプリケーションのコンパイルおよび起動に使用できるカスタムコマンドも表示されます。
  10. Download the ZIP を選択して、生成されたプロジェクトファイルを含むアーカイブをマシンに保存します。
  11. アーカイブの内容を展開します。
  12. 展開したプロジェクトファイルが含まれるディレクトリーに移動します。

    cd <directory_name>
  13. 開発モードでアプリケーションをコンパイルして起動します。

    ./mvnw compile quarkus:dev

6.4. Quarkus CLI を使用した Quarkus プラットフォームでの Red Hat build of OptaPlanner プロジェクトの作成

Quarkus コマンドラインインターフェイス (CLI) を使用して、Quarkus OptaPlanner プロジェクトを作成できます。

前提条件

手順

  1. Quarkus アプリケーションを作成します。

    quarkus create app -P io.quarkus:quarkus-bom:2.13.7.SP1-redhat-0000x
  2. 利用可能な拡張機能を表示するには、以下のコマンドを入力します。

    quarkus ext -i

    このコマンドは、以下の拡張機能を返します。

    optaplanner-quarkus
    optaplanner-quarkus-benchmark
    optaplanner-quarkus-jackson
    optaplanner-quarkus-jsonb
  3. 以下のコマンドを入力して、エクステンションをプロジェクトの pom.xml ファイルに追加します。

    quarkus ext add resteasy-jackson
    quarkus ext add optaplanner-quarkus
    quarkus ext add optaplanner-quarkus-jackson
  4. テキストエディターで pom.xml ファイルを開きます。ファイルの内容は以下の例のようになります。

    <?xml version="1.0"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <modelVersion>4.0.0</modelVersion>
      <groupId>org.acme</groupId>
      <artifactId>code-with-quarkus-optaplanner</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      <properties>
    	<compiler-plugin.version>3.8.1</compiler-plugin.version>
    	<maven.compiler.parameters>true</maven.compiler.parameters>
    	<maven.compiler.source>11</maven.compiler.source>
    	<maven.compiler.target>11</maven.compiler.target>
    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    	<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
    	<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
    	<quarkus.platform.version>2.13.7.SP1-redhat-0000x</quarkus.platform.version>
    	<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
      </properties>
      <dependencyManagement>
    	<dependencies>
      	<dependency>
        	<groupId>${quarkus.platform.group-id}</groupId>
        	<artifactId>${quarkus.platform.artifact-id}</artifactId>
        	<version>${quarkus.platform.version}</version>
        	<type>pom</type>
        	<scope>import</scope>
      	</dependency>
      	<dependency>
        	<groupId>io.quarkus.platform</groupId>
        	<artifactId>optaplanner-quarkus</artifactId>
        	<version>2.2.2.Final</version>
        	<type>pom</type>
        	<scope>import</scope>
      	</dependency>
    	</dependencies>
      </dependencyManagement>
      <dependencies>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-arc</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-resteasy</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>org.optaplanner</groupId>
      	<artifactId>optaplanner-quarkus</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>org.optaplanner</groupId>
      	<artifactId>optaplanner-quarkus-jackson</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-resteasy-jackson</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-junit5</artifactId>
      	<scope>test</scope>
    	</dependency>
    	<dependency>
      	<groupId>io.rest-assured</groupId>
      	<artifactId>rest-assured</artifactId>
      	<scope>test</scope>
    	</dependency>
      </dependencies>
      <build>
    	<plugins>
      	<plugin>
        	<groupId>${quarkus.platform.group-id}</groupId>
        	<artifactId>quarkus-maven-plugin</artifactId>
        	<version>${quarkus.platform.version}</version>
        	<extensions>true</extensions>
        	<executions>
          	<execution>
            	<goals>
              	<goal>build</goal>
              	<goal>generate-code</goal>
              	<goal>generate-code-tests</goal>
            	</goals>
          	</execution>
        	</executions>
      	</plugin>
      	<plugin>
        	<artifactId>maven-compiler-plugin</artifactId>
        	<version>${compiler-plugin.version}</version>
        	<configuration>
          	<parameters>${maven.compiler.parameters}</parameters>
        	</configuration>
      	</plugin>
      	<plugin>
        	<artifactId>maven-surefire-plugin</artifactId>
        	<version>${surefire-plugin.version}</version>
        	<configuration>
          	<systemPropertyVariables>
            	<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
            	<maven.home>${maven.home}</maven.home>
          	</systemPropertyVariables>
        	</configuration>
      	</plugin>
    	</plugins>
      </build>
      <profiles>
    	<profile>
      	<id>native</id>
      	<activation>
        	<property>
          	<name>native</name>
        	</property>
      	</activation>
      	<build>
        	<plugins>
          	<plugin>
            	<artifactId>maven-failsafe-plugin</artifactId>
            	<version>${surefire-plugin.version}</version>
            	<executions>
              	<execution>
                	<goals>
                  	<goal>integration-test</goal>
                  	<goal>verify</goal>
                	</goals>
                	<configuration>
                  	<systemPropertyVariables>
                    	<native.image.path>${project.build.directory}/${project.build.finalName}-run</native.image.path>
                    	<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    	<maven.home>${maven.home}</maven.home>
                  	</systemPropertyVariables>
                	</configuration>
              	</execution>
            	</executions>
          	</plugin>
        	</plugins>
      	</build>
      	<properties>
        	<quarkus.package.type>native</quarkus.package.type>
      	</properties>
    	</profile>
      </profiles>
    </project>

パート III. Red Hat build of OptaPlanner のソルバー

OptaPlanner を使用して計画の問題を解決するには、次の手順を実行します。

  1. @PlanningSolution アノテーションでアノテーションが付けられたクラスとして プランニングの問題をモデル化します (例: NQueens クラス)。
  2. Solver を設定します (NQueens インスタンスの First Fit や Tabu Search Solver など)。
  3. データレイヤーから問題データセットを読み込みます (例: Queens インスタンス)。これがプラニング問題です。
  4. 見つかった最善解を返す Solver.solve (problem) 解決します。
SolverFactory の入出力ワークフローの図

第7章 Red Hat Build of OptaPlanner ソルバーの設定

以下の方法を使用して、OptaPlanner のソルバーを設定できます。

  • XML ファイルを使用します。
  • SolverConfig API を使用します。
  • ドメインモデルにクラスアノテーションと JavaBean プロパティーアノテーションを追加します。
  • OptaPlanner がドメインにアクセスするために使用するメソッドを制御します。
  • カスタムプロパティーを定義します。

7.1. XML ファイルを使用した OptaPlanner のソルバーの設定

各サンプルプロジェクトには、編集可能なソルバー設定ファイルがあります。<EXAMPLE>SolverConfig.xml ファイルは、org.optaplanner.optaplanner-8.33.0.Final-redhat-00004/optaplanner-examples/src/main/resources/org/optaplanner/examples/<EXAMPLE>ディレクトリーにあります。<EXAMPLE> は OptaPlanner サンプルプロジェクトの名前です。または、SolverFactory.createFromXmlFile() で ファイルから SolverFactory を作成することもできます。ただし、移植性の理由から、クラスパスのリソースが推奨されます。

SolverSolverFactory の両方に、 Solution_ と呼ばれるジェネリック型があります。これは、計画の問題と解決策を表すクラスです。

OptaPlanner を使用すると、設定を変更することで、最適化アルゴリズムを比較的簡単に切り替えることができます。

手順

  1. SolverFactorySolver インスタンスをビルドします。
  2. Solver 設定の XML ファイルを設定します。

    1. モデルを定義します。
    2. スコア機能を定義します。
    3. 必要に応じて、最適化アルゴリズムを設定します。

      以下の例は、NQueens 問題に対するソルバー XML ファイルです。

      <?xml version="1.0" encoding="UTF-8"?>
      <solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
        <!-- Define the model -->
        <solutionClass>org.optaplanner.examples.nqueens.domain.NQueens</solutionClass>
        <entityClass>org.optaplanner.examples.nqueens.domain.Queen</entityClass>
      
        <!-- Define the score function -->
        <scoreDirectorFactory>
          <scoreDrl>org/optaplanner/examples/nqueens/optional/nQueensConstraints.drl</scoreDrl>
        </scoreDirectorFactory>
      
        <!-- Configure the optimization algorithms (optional) -->
        <termination>
          ...
        </termination>
        <constructionHeuristic>
          ...
        </constructionHeuristic>
        <localSearch>
          ...
        </localSearch>
      </solver>
      注記

      一部の環境では、OSGi や JBoss モジュールなどの一部の環境では、JAR ファイルのソルバー設定、スコア DRL、ドメインクラスなどのクラスパスリソースが optaplanner-core JAR ファイルのデフォルトの ClassLoader で利用できない場合もあります。このような場合は、クラスの ClassLoader をパラメーターとして提供します。

             SolverFactory<NQueens> solverFactory = SolverFactory.createFromXmlResource(
                     ".../nqueensSolverConfig.xml", getClass().getClassLoader());
  3. ClassLoader.getResource() で定義されているクラスパスリソースとして提供されるソルバー設定 XML ファイルを使用して SolverFactory を設定します。

           SolverFasctory<NQueens> solverFactory = SolverFactory.createFromXmlResource(
                   "org/optaplanner/examples/nqueens/optional/nqueensSolverConfig.xml");
           Solver<NQueens> solver = solverFactory.buildSolver();

7.2. Java API を使用した OptaPlanner のソルバーの設定

SolverConfig API を使用して Solver を設定できます。これは特に、ランタイム時に値を動的に変更する場合に便利です。以下の例では、NQueens プロジェクトで Solver を構築する前のシステムプロパティーに基づいて実行時間を変更します。

        SolverConfig solverConfig = SolverConfig.createFromXmlResource(
                "org/optaplanner/examples/nqueens/optional/nqueensSolverConfig.xml");
        solverConfig.withTerminationConfig(new TerminationConfig()
                        .withMinutesSpentLimit(userInput));

        SolverFactory<NQueens> solverFactory = SolverFactory.create(solverConfig);
        Solver<NQueens> solver = solverFactory.buildSolver();

ソルバー設定 XML ファイルのすべての要素は、パッケージ namespace org.optaplanner.core.configConfig クラスまたは プロパティー として使用できます。これらの Config クラスは XML 形式の Java 表現です。これらのコンポーネントには、パッケージ namespace org.optaplanner.core.impl のランタイムコンポーネントをビルドし、それらを効率的な Solver に組み込むことができます。

注記

各ユーザー要求に SolverFactory を動的に設定するには、初期化中にテンプレート SolverConfig をビルドし、各ユーザー要求のコピーコンストラクターでコピーします。以下の例は、NQueens の問題でこれを実行する方法を示しています。

    private SolverConfig template;

    public void init() {
        template = SolverConfig.createFromXmlResource(
                "org/optaplanner/examples/nqueens/optional/nqueensSolverConfig.xml");
        template.setTerminationConfig(new TerminationConfig());
    }

    // Called concurrently from different threads
    public void userRequest(..., long userInput) {
        SolverConfig solverConfig = new SolverConfig(template); // Copy it
        solverConfig.getTerminationConfig().setMinutesSpentLimit(userInput);
        SolverFactory<NQueens> solverFactory = SolverFactory.create(solverConfig);
        Solver<NQueens> solver = solverFactory.buildSolver();
        ...
    }

7.3. OptaPlanner アノテーション

ドメインモデルのクラスは、プランニング変数などのプランニングエンティティーを指定する必要があります。以下の方法のいずれかを使用して、OptaPlanner プロジェクトにアノテーションを追加します。

  • ドメインモデルにクラスアノテーションと JavaBean プロパティーアノテーションを追加します。プロパティーアノテーションは setter メソッドではなく getter メソッドに配置する必要があります。アノテーションが付けられた getter メソッドの公開は必要ありません。これは推奨される方法です。
  • ドメインモデルにクラスアノテーションとフィールドアノテーションを追加します。アノテーションが付けられたフィールドはパブリックである必要はありません。

7.4. OptaPlanner ドメインアクセスの指定

デフォルトでは、OptaPlanner はリフレクションを使用してドメインにアクセスします。リフレクションは信頼性がありますが、直接アクセスに比べると遅くなります。または、Gizmo を使用してドメインにアクセスするように OptaPlanner を設定できます。これにより、リフレクションなしにドメインのフィールドとメソッドに直接アクセスするバイトコードが生成されます。ただし、この手法には以下の制限があります。

  • プランニングアノテーションは、パブリックフィールドおよびパブリック getter でのみ指定できます。
  • io.quarkus.gizmo:gizmo はクラスパス上にある必要があります。
注記

Gizmo がデフォルトのドメインアクセスタイプであるため、Quarkus で OptaPlanner を使用する場合、これらの制限は適用されません。

手順

Quarkus の外部にある Gizmo を使用するには、ソルバー設定で domainAccessType を設定します。

  <solver>
    <domainAccessType>GIZMO</domainAccessType>
  </solver>

7.5. カスタムプロパティーの設定

OptaPlanner プロジェクトでは、クラスをインスタンス化し、カスタムプロパティーに明示的に言及するドキュメントを持つソルバー設定要素にカスタムプロパティーを追加できます。

前提条件

  • ソルバーがあること。

手順

  1. カスタムプロパティーを追加します。

    たとえば、Easy ScoreCalculator にキャッシュされる大きな計算があり、1 つのベンチマークでキャッシュサイズを増やす場合は、myCacheSize プロパティーを追加します。

      <scoreDirectorFactory>
        <easyScoreCalculatorClass>...MyEasyScoreCalculator</easyScoreCalculatorClass>
        <easyScoreCalculatorCustomProperties>
          <property name="myCacheSize" value="1000"/><!-- Override value -->
        </easyScoreCalculatorCustomProperties>
      </scoreDirectorFactory>
  2. カスタムプロパティーごとにパブリックセッターを追加します。これは、ソルバーの ビルド時に呼び出されます。

    public class MyEasyScoreCalculator extends EasyScoreCalculator<MySolution, SimpleScore> {
    
            private int myCacheSize = 500; // Default value
    
            @SuppressWarnings("unused")
            public void setMyCacheSize(int myCacheSize) {
                this.myCacheSize = myCacheSize;
            }
    
        ...
    }

    booleanintdoubleBigDecimalStringenums など、ほとんどの値型がサポートされています。

第8章 OptaPlanner ソルバー

ソルバーは、計画の問題に対する最適で最適なソリューションを見つけます。ソルバーで解決できる計画問題インスタンスは度に 1 つずつです。ソルバーは、SolverFactory メソッドを使用して構築されます。

public interface Solver<Solution_> {

    Solution_ solve(Solution_ problem);

    ...
}

スレッドセーフであることが javadoc に具体的に記載されているメソッドを除いて、ソルバーには単一のスレッドからアクセスする必要があります。solve() メソッドは、現在のスレッドを占有します。スレッドを占有すると、REST サービスの HTTP タイムアウトが発生する可能性があり、複数のデータセットを並行して解決するために追加のコードが必要になります。このような問題を回避するには、代わりに SolverManager を使用してください。

8.1. 問題の解決

ソルバーを使用して、計画の問題を解決します。

前提条件

  • ソルバー設定で構築された Solver
  • 計画問題インスタンスを表す @PlanningSolution アノテーション

手順

計画問題を solve() メソッドの引数として提供します。ソルバーは、見つかった最良の解を返します。

次の例は、NQueens の問題を解決します。

    NQueens problem = ...;
    NQueens bestSolution = solver.solve(problem);

この例では、 solve() メソッドは、すべての QueenRow に割り当てられた NQueens インスタンスを返します。

注記

solve (Solution) メソッドに指定されたソリューションインスタンスは、部分的または完全に初期化できます。これは、繰り返して計画する場合によくあります。

図8.1 8ms のフォークイーンズパズルのベストソリューション (最適なソリューションでもあります)

8 ミリ秒で 4 個のクィーンパズルを解決する最適解

solve (Solution) メソッドは、問題のサイズとソルバーの設定によっては時間がかかる場合があります。Solver は、可能な解決策の検索スペースをインテリジェントに処理し、解決中に遭遇した最良の解決策を記憶します。問題の大きさ、Solver が持っているどのくらいの時間、ソルバーの設定、等々の数ある要因により、最善の 解決策は、最適な 解決策ではない可能性もあります、

注記

メソッド solve (Solution) に 与えられたソリューションインスタンスは Solver によって変更されますが、それを最良のソリューションと間違えないでください。

solve (Solution) または getBestSolution() メソッドによって返されたソリューションのインスタンスは、solve (Solution) に渡された計画インスタンスのクローンである可能性が高いです。この場合、つまりは別のインスタンスであることを意味しています。

8.2. ソルバー環境モード

ソルバー環境モードを使用すると、実装の一般的なバグを検出できます。ロギングレベルには影響しません。

ソルバーには単一のランダムインスタンスがあります。一部のソルバー設定は、他の設定よりもランダムインスタンスを多く使用します。たとえば、シミュレーテッドアニーリングアルゴリズムは乱数に大きく依存しますが、Tabu Search はスコアの同点を解決するために乱数にのみ依存します。環境モードは、そのランダムインスタンスのシードに影響を与えます。

ソルバー設定 XML ファイルで環境モードを設定できます。次の例では、 FAST_ASSERT モードを設定します。

<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
  <environmentMode>FAST_ASSERT</environmentMode>
  ...
</solver>

次のリストは、ソルバー設定ファイルで使用できる環境モードについて説明しています。

  • FULL_ASSERT モードは、すべてのアサーションをオンにします。たとえば、増分スコアの計算が移動ごとに破損していないというアサーション、Move 実装のバグ、制約、エンジン自体などでフェイルファストします。このモードは再現可能です。また、非アサートモードよりも頻繁に calculateScore() メソッドを呼び出すため、煩わしいものです。FULL_ASSERT モードは、増分スコア計算に依存しないため、非常に低速です。
  • NON_INTRUSIVE_FULL_ASSERT モードは、Move 実装のバグ、制約、エンジン自体などでフェイルファストするためにいくつかのアサーションをオンにします。このモードは再現可能です。非アサートモードよりも頻繁に calculateScore() メソッドを呼び出さないため、邪魔になりません。NON_INTRUSIVE_FULL_ASSERT モードは、増分スコア計算に依存しないため、非常に低速です。
  • FAST_ASSERT モードは、undoMove のスコアが Move の前と同じであるというアサーションなど、ほとんどのアサーションをオンにして、Move 実装のバグ、制約、エンジン自体などをフェイルファストします。このモードは再現可能です。また、非アサートモードよりも頻繁に calculateScore() メソッドを呼び出すため、煩わしいものです。FAST_ASSERT モードは遅いです。FAST_ASSERT モードをオンにして、計画の問題を短時間実行するテストケースを作成します。
  • REPRODUCIBLE モードは、開発中に推奨されるため、デフォルトのモードです。このモードでは、同じ OptaPlanner バージョンで 2 回実行すると、同じコードが同じ順序で実行されます。次の注意事項が当てはまる場合を除いて、これら 2 つの実行はすべてのステップで同じ結果になります。これにより、バグを一貫して再現できます。また、スコア制約のパフォーマンスの最適化など、特定のリファクタリングを実行全体で公平にベンチマークすることもできます。

    注記

    REPRODCIBLE モードを使用しているにもかかわらず、次の理由により、アプリケーションが完全に再現できない場合があります。

    • 特にソリューションの実装において、計画エンティティーまたは計画値のコレクションに対して、JVM 実行間で順序が一貫していないが、通常の問題の事実ではない HashSet または別の コレクション を使用する。LinkedHashSet に置き換えます。
    • 時間勾配に依存するアルゴリズム、特にシミュレーテッドアニーリングアルゴリズムを、終了に費やした時間と組み合わせます。割り当てられた CPU 時間大きな違いがあると、時間勾配値に影響を与えます。シミュレーテッドアニーリングアルゴリズムをレイトアクセプタンスアルゴリズムに置き換えるか、終了に費やした時間をステップカウント終了に置き換えます。
  • REPRODUCIBLE モードは、NON_REPRODUCIBLE モードよりもわずかに遅くなる可能性があります。実稼働環境で再現性の恩恵を受けることができる場合は、実稼働でこのモードを使用してください。実際には、 REPRODUCIBLE モードでは、シードが指定されていない場合、デフォルトの固定ランダムシードが使用され、ワークスティーリングなどの特定の同時実行の最適化も無効になります。
  • NON_REPRODUCIBLE モードは、REPRODUCIBLE モードよりもわずかに高速です。デバッグやバグ修正が困難になるため、開発中の使用は避けてください。実稼働環境で再現性が重要でない場合は、実稼働で NON_REPRODUCIBLE モードを使用してください。実際には、シードが指定されていない場合、このモードは固定ランダムシードを使用しません。

8.3. OptaPlanner ソルバーのログレベルの変更

OptaPlanner ソルバーのログレベルを変更して、ソルバーアクティビティーを確認できます。次のリストは、さまざまなログレベルについて説明しています。

  • error:RuntimeException として呼び出し元のコードに throw されるエラーを除いて、エラーをログに記録します。

    エラーが発生した場合、OptaPlanner は通常は短時間で失敗します。呼び出し元のコードに詳細なメッセージを含む RuntimeException の サブクラスを出力します。ログメッセージの重複を避けるために、エラーとしてログに記録されません。呼び出し元のコードがその RuntimeException を 明示的にキャッチして排除しない限り、スレッドのデフォルトの `ExceptionHandler はとにかくそれをエラーとしてログに記録します。その間、コードはさらに害を及ぼしたり、エラーを難読化したりすることで中断されます。

  • 警告: 疑わしい状況をログに記録します
  • info: すべてのフェーズとソルバー自体をログに記録します
  • デバッグ: すべてのフェーズのすべてのステップをログに記録します
  • トレース: すべてのフェーズのすべてのステップのすべての動きをログに記録します
注記

トレース ログを指定すると、パフォーマンスが大幅に低下します。ただし、トレース ロギングは、ボトルネックを発見するための開発中に非常に重要です。

デバッグ ログでさえ、レイトアクセプタンスやシミュレーテッドアニーリングなどの高速ステッピングアルゴリズムではパフォーマンスが大幅に低下する可能性がありますが、タブーサーチなどの低速ステッピングアルゴリズムでは低下しません。

trace`と デバッグ ロギングの両方が、ほとんどのアペンダーでのマルチスレッド解決で輻輳を引き起こします。

Eclipse では、コンソールへの デバッグ ログにより、スコア計算速度が 1 秒あたり 10000 を超える輻輳が発生する傾向があります。IntelliJ も Maven コマンドラインもこの問題に悩まされていません。

手順

ロギングレベルを デバッグ ロギングに設定して、フェーズがいつ終了し、どのくらいの速さでステップが実行されるかを確認します。

次の例は、デバッグログからの出力を示しています。

INFO  Solving started: time spent (3), best score (-4init/0), random (JDK with seed 0).
DEBUG     CH step (0), time spent (5), score (-3init/0), selected move count (1), picked move (Queen-2 {null -> Row-0}).
DEBUG     CH step (1), time spent (7), score (-2init/0), selected move count (3), picked move (Queen-1 {null -> Row-2}).
DEBUG     CH step (2), time spent (10), score (-1init/0), selected move count (4), picked move (Queen-3 {null -> Row-3}).
DEBUG     CH step (3), time spent (12), score (-1), selected move count (4), picked move (Queen-0 {null -> Row-1}).
INFO  Construction Heuristic phase (0) ended: time spent (12), best score (-1), score calculation speed (9000/sec), step total (4).
DEBUG     LS step (0), time spent (19), score (-1),     best score (-1), accepted/selected move count (12/12), picked move (Queen-1 {Row-2 -> Row-3}).
DEBUG     LS step (1), time spent (24), score (0), new best score (0), accepted/selected move count (9/12), picked move (Queen-3 {Row-3 -> Row-2}).
INFO  Local Search phase (1) ended: time spent (24), best score (0), score calculation speed (4000/sec), step total (2).
INFO  Solving ended: time spent (24), best score (0), score calculation speed (7000/sec), phase total (2), environment mode (REPRODUCIBLE).

費やされた時間の値はすべてミリ秒単位です。

すべてが SLF4J に記録されます。これは、すべてのログメッセージを Logback、Apache Commons Logging、Log4j、または java.util.logging に委任する単純なログファサードです。選択したロギングフレームワークのロギングアダプターに依存関係を追加します。

8.4. Logback を使用して OptaPlanner ソルバーアクティビティーをログに記録する

Logback は、OptaPlanner で使用するために推奨されるロギングフレームワークです。Logback を使用して、OptaPlanner ソルバーアクティビティーをログに記録します。

前提条件

  • OptaPlanner プロジェクトがあります。

手順

  1. 次の Maven 依存関係を OptaPlanner プロジェクトの pom.xml ファイルに追加します。

    注記

    ブリッジの依存関係を追加する必要はありません。

        <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>1.x</version>
        </dependency>
  2. 次の例に示すように、 logback.xml ファイルの org.optaplanner パッケージのログレベルを設定します。ここで、<LEVEL>「Logback を使用して OptaPlanner ソルバーアクティビティーをログに記録する」 にリストされているログレベルです。。

    <configuration>
    
      <logger name="org.optaplanner" level="<LEVEL>"/>
    
      ...
    
    </configuration>
  3. オプション: 複数の ソルバー インスタンスが同時に実行されている可能性があるマルチテナントアプリケーションがある場合は、各インスタンスのログを別々のファイルに分割します。

    1. solve() 呼び出しをマップされた診断コンテキスト (MDC) で囲みます。

              MDC.put("tenant.name",tenantName);
              MySolution bestSolution = solver.solve(problem);
              MDC.remove("tenant.name");
    2. ${tenant.name} ごとに異なるファイルを使用するようにロガーを設定します。たとえば、 logback.xml ファイルで SiftingAppender を使用します。

        <appender name="fileAppender" class="ch.qos.logback.classic.sift.SiftingAppender">
          <discriminator>
            <key>tenant.name</key>
            <defaultValue>unknown</defaultValue>
          </discriminator>
          <sift>
            <appender name="fileAppender.${tenant.name}" class="...FileAppender">
              <file>local/log/optaplanner-${tenant.name}.log</file>
              ...
            </appender>
          </sift>
        </appender>
      注記

      複数のソルバーまたは 1 つのマルチスレッドソルバーを実行する場合、コンソールを含むほとんどのアペンダーは、デバッグ および トレース ログで輻輳を引き起こします。この問題を回避するには、非同期アペンダーに切り替えるか、デバッグ ログをオフにします。

  4. OptaPlanner が新しいレベルを認識しない場合は、一時的にシステムプロパティー -Dlogback.LEVEL=true を追加してトラブルシューティングします。

8.5. Log4J を使用して OptaPlanner ソルバーアクティビティーをログに記録する

すでに Log4J を使用していて、より高速な後継である Logback に切り替えたくない場合は、Log4J 用に OptaPlanner プロジェクトを設定できます。

前提条件

  • OptaPlanner プロジェクトがあります
  • Log4J ロギングフレームワークを使用しています

手順

  1. ブリッジの依存関係をプロジェクトの pom.xml ファイルに追加します。

        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.x</version>
        </dependency>
  2. 次の例に示すように、log4j.xml ファイルのパッケージ org.optaplanner でログレベルを設定します。ここで、<LEVEL>「Logback を使用して OptaPlanner ソルバーアクティビティーをログに記録する」 にリストされているログレベルです。

    <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    
      <category name="org.optaplanner">
        <priority value="<LEVEL>" />
      </category>
    
      ...
    
    </log4j:configuration>
  3. オプション: 複数の ソルバー インスタンスが同時に実行されている可能性があるマルチテナントアプリケーションがある場合は、各インスタンスのログを別々のファイルに分割します。

    1. solve() 呼び出しをマップされた診断コンテキスト (MDC) で囲みます。

              MDC.put("tenant.name",tenantName);
              MySolution bestSolution = solver.solve(problem);
              MDC.remove("tenant.name");
    2. ${tenant.name} ごとに異なるファイルを使用するようにロガーを設定します。たとえば、 logback.xml ファイルで SiftingAppender を使用します。

        <appender name="fileAppender" class="ch.qos.logback.classic.sift.SiftingAppender">
          <discriminator>
            <key>tenant.name</key>
            <defaultValue>unknown</defaultValue>
          </discriminator>
          <sift>
            <appender name="fileAppender.${tenant.name}" class="...FileAppender">
              <file>local/log/optaplanner-${tenant.name}.log</file>
              ...
            </appender>
          </sift>
        </appender>
      注記

      複数のソルバーまたは 1 つのマルチスレッドソルバーを実行する場合、コンソールを含むほとんどのアペンダーは、デバッグ および トレース ログで輻輳を引き起こします。この問題を回避するには、非同期アペンダーに切り替えるか、デバッグ ログをオフにします。

8.6. ソルバーの監視

OptaPlanner は、Java アプリケーション用のメトリック計測ライブラリーである Micrometer を介してメトリックを公開します。一般的な監視システムで Micrometer を使用して、OptaPlanner ソルバーを監視できます。

8.6.1. Micrometer 用の Quarkus OptaPlanner アプリケーションの設定

OptaPlanner Quarkus アプリケーションを Micrometer および指定された監視システムを使用するように設定するには、Micrometer 依存関係を pom.xml ファイルに追加します。

前提条件

  • Quarkus OptaPlanner アプリケーションがあります。

手順

  1. 次の依存関係をアプリケーションの pom.xml ファイルに追加します。ここで <MONITORING_SYSTEM> は Micrometer と Quarkus でサポートされている監視システムです。

    注記

    Prometheus は現在、Quarkus でサポートされている唯一の監視システムです。

    <dependency>
     <groupId>io.quarkus</groupId>
     <artifactId>quarkus-micrometer-registry-<MONITORING_SYSTEM></artifactId>
    </dependency>
  2. アプリケーションを開発モードで実行するには、次のコマンドを入力します。

    mvn compile quarkus:dev
  3. アプリケーションのメトリックを表示するには、ブラウザーに次の URL を入力します。

    http://localhost:8080/q/metrics

8.6.2. Micrometer 用の Spring Boot OptaPlanner アプリケーションの設定

Micrometer と指定された監視システムを使用するように Spring Boot OptaPlanner アプリケーションを設定するには、Pom.xml ファイルに Micrometer 依存関係を追加します。

前提条件

  • Spring Boot OptaPlanner アプリケーションがあります。

手順

  1. 次の依存関係をアプリケーションの pom.xml ファイルに追加します。ここで <MONITORING_SYSTEM> は Micrometer と Spring Boot でサポートされている監視システムです。

    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-<MONITORING_SYSTEM></artifactId>
    </dependency>
  2. アプリケーションの application.properties ファイルに設定情報を追加します。詳細は、Micrometer Web サイトを参照してください。
  3. アプリケーションを実行するには、以下のコマンドを入力します。

    mvn spring-boot:run
  4. アプリケーションのメトリックを表示するには、ブラウザーに次の URL を入力します。

    http://localhost:8080/actuator/metrics

    注記

    次の URL を Prometheus スクレイパーパスとして使用します: http://localhost:8080/actuator/prometheus

8.6.3. Micrometer 用のプレーンな Java OptaPlanner アプリケーションの設定

Micrometer を使用するようにプレーンな Java OptaPlanner アプリケーションを設定するには、Micrometer の依存関係と、選択した監視システムの設定情報をプロジェクトの POM.XML ファイルに追加する必要があります。

前提条件

  • プレーンな Java OptaPlanner アプリケーションがあります。

手順

  1. 次の依存関係をアプリケーションの pom.xml ファイルに追加します。ここで、<MONITORING_SYSTEM> は Micrometer で設定された監視システムであり、<VERSION> は使用している Micrometer のバージョンです。

    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-<MONITORING_SYSTEM></artifactId>
     <version><VERSION></version>
    </dependency>
    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-core</artifactId>
     <version><VERSION></version>
    </dependency>
  2. プロジェクトの pom.xml ファイルの先頭に、監視システムの Micrometer 設定情報を追加します。詳細は、Micrometer Web サイトを参照してください。
  3. 設定情報の下に次の行を追加します。ここで、<MONITORING_SYSTEM> は追加した監視システムです。

    Metrics.addRegistry(<MONITORING_SYSTEM>);

    次の例は、Prometheus 監視システムを追加する方法を示しています。

    PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    try {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/prometheus", httpExchange -> {
            String response = prometheusRegistry.scrape();
            httpExchange.sendResponseHeaders(200, response.getBytes().length);
            try (OutputStream os = httpExchange.getResponseBody()) {
                os.write(response.getBytes());
            }
        });
        new Thread(server::start).start();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    Metrics.addRegistry(prometheusRegistry);
  4. 監視システムを開いて、OptaPlanner プロジェクトのメトリックを表示します。次のメトリックが公開されます。

    注記

    メトリックの名前と形式は、レジストリーによって異なります。

    • optaplanner.solver.errors.total: 測定開始以降に解決中に発生したエラーの総数。
    • optaplanner.solver.solve-length.active-count: 現在解いているソルバーの数。
    • optaplanner.solver.solve-length.seconds-max: 現在アクティブなソルバーの実行時間が最も長い実行時間。
    • optaplanner.solver.solve-length.seconds-duration-sum: アクティブな各ソルバーの解決時間の合計。たとえば、アクティブなソルバーが 2 つあり、一方が 3 分間実行され、もう一方が 1 分間実行されている場合、合計計算時間は 4 分です。

8.6.4. 追加メトリクス

より詳細な監視を行うには、ソルバー設定で OptaPlanner を設定して、パフォーマンスコストで追加のメトリックを監視できます。次の例では、BEST_SCORE および SCORE_CALCULATION_COUNT メトリクスを使用しています。

<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
  <monitoring>
    <metric>BEST_SCORE</metric>
    <metric>SCORE_CALCULATION_COUNT</metric>
    ...
  </monitoring>
  ...
</solver>

この設定では、次のメトリックを有効にできます。

  • SOLVE_DURATION (デフォルトで有効、マイクロメータメーター ID: optaplanner.solver.solve.duration): 最長のアクティブソルバーの解決時間、アクティブソルバーの数、アクティブなすべてのソルバーの累積時間を測定します。
  • ERROR_COUNT (デフォルトで有効、マイクロメーターメーター ID: optaplanner.solver.errors): 解決中に発生したエラーの数を測定します。
  • SCORE_CALCULATION_COUNT (デフォルトで有効、マイクロメーターメーター ID: optaplanner.solver.score.calculation.count): OptaPlanner が実行したスコア計算の数を測定します。
  • BEST_SCORE (マイクロメーターメーター ID: optaplanner.solver.best.score.*): OptaPlanner がこれまでに見つけた最適解のスコアを測定します。スコアのレベルごとに個別のメーターがあります。たとえば、HardSoftScore の場合、optaplanner.solver.best.score.hard.score および optaplanner.solver.best.score.soft.score メーターがあります。
  • STEP_SCORE (マイクロメーターメーター ID: optaplanner.solver.step.score.*): OptaPlanner が実行する各ステップのスコアを測定します。スコアのレベルごとに個別のメーターがあります。たとえば、HardSoftScore の場合、optaplanner.solver.step.score.hard.score および optaplanner.solver.step.score.soft.score メーターがあります。
  • BEST_SOLUTION_MUTATION (マイクロメーターメーター ID: optaplanner.solver.best.solution.mutation): 連続する最適解の間で変更された計画変数の数を測定します。
  • MOVE_COUNT_PER_STEP (マイクロメータメーター ID: optaplanner.solver.step.move.count): ステップで評価された移動の数を測定します。
  • MEMORY_USE (マイクロメーターメーター ID: jvm.memory.used): JVM 全体で使用されるメモリーの量を測定します。このメトリクスは、ソルバーが使用するメモリーの量を測定するものではありません。同じ JVM 上の 2 つのソルバーは、このメトリックに対して同じ値を報告します。
  • CONSTRAINT_MATCH_TOTAL_BEST_SCORE (マイクロメーターメーター ID: optaplanner.solver.constraint.match.best.score.*): OptaPlanner がこれまでに見つけた最適解に対する各制約のスコアの影響を測定します。スコアのレベルごとに個別のメーターがあり、各制約のタグが付いています。たとえば、パッケージ com.example の制約 Minimize Cost の HardSoftScore には、 optaplanner.solver.constraint.match.best.score.hard.scoreoptaplanner.solver.constraint.match.best.score.soft.score があります。これらは、タグ "constraint.package=com.example" と "constraint.name=Minimize Cost" を持ちます。
  • CONSTRAINT_MATCH_TOTAL_STEP_SCORE (マイクロメーターメーター ID: optaplanner.solver.constraint.match.step.score.*): 現在のステップに対する各制約のスコアの影響を測定します。スコアのレベルごとに個別のメーターがあり、各制約のタグが付いています。たとえば、パッケージ com.example の制約 Minimize Cost の HardSoftScore には、optaplanner.solver.constraint.match.step.score.hard.scoreoptaplanner.solver.constraint.match.step.score.soft.score があります。これらには、タグ "constraint.package=com.example" と "constraint.name=Minimize Cost" があります。
  • PICKED_MOVE_TYPE_BEST_SCORE_DIFF (マイクロメーターメーター ID: optaplanner.solver.move.type.best.score.diff.*): 特定の移動タイプが最適解をどの程度改善するかを測定します。スコアのレベルごとに個別のメーターがあり、移動タイプのタグが付いています。たとえば、プロセスのコンピューターの HardSoftScoreChangeMove には、optaplanner.solver.move.type.best.score.diff.hard.scoreoptaplanner.solver.move.type.best.score.diff.soft.score メーターがあります。これには、move.type=ChangeMove(Process.computer) タグがあります。
  • PICKED_MOVE_TYPE_STEP_SCORE_DIFF (マイクロメーターメーター ID: optaplanner.solver.move.type.step.score.diff.*): 特定の移動タイプが最適解をどの程度改善するかを測定します。スコアのレベルごとに個別のメーターがあり、移動タイプのタグが付いています。たとえば、プロセスのコンピューターの HardSoftScoreChangeMove には、optaplanner.solver.move.type.step.score.diff.hard.scoreoptaplanner.solver.move.type.step.score.diff.soft.scoreメーターがあります。これには、タグ move.type=ChangeMove(Process.computer) が含まれます。

8.7. 乱数ジェネレーターの設定

多くのヒューリスティックおよびメタヒューリスティックは、移動の選択、スコアの結びつきの解決、確率ベースの移動の受け入れなどを疑似乱数ジェネレーターに依存しています。解決中に、同じランダムインスタンスが再利用され、再現性、パフォーマンス、およびランダム値の均一な分布が向上します。

ランダムシードは、疑似乱数ジェネレータを初期化するために使用される番号です。

手順

  1. オプション: ランダムインスタンスのランダムシードを変更するには、 randomSeed を指定します。

    <solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
      <randomSeed>0</randomSeed>
      ...
    </solver>
  2. オプション: 疑似乱数ジェネレーターの実装を変更するには、以下のソルバー設定ファイルにリストされている randomType プロパティーの値を指定します。ここで、<RANDOM_NUMBER_GENERATOR> は疑似乱数ジェネレーターです。

    <solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
      <randomType><RANDOM_NUMBER_GENERATOR></randomType>
      ...
    </solver>

    次の疑似乱数ジェネレータがサポートされています。

    • JDK (デフォルト): 標準の乱数ジェネレーターの実装 (java.util.Random)
    • MERSENNE_TWISTER:コモンズ数学 によって乱数ジェネレータの実装
    • WELL512A, WELL1024A, WELL19937A, WELL19937C, WELL44497AWELL44497B: Commons Mathによる乱数ジェネレータの実装

ほとんどのユースケースでは、 randomType プロパティーの値は、複数のデータセットでの最良のソリューションの平均品質に大きな影響を与えません。

第9章 OptaPlanner SolverManager

SolverManager は、REST およびその他のエンタープライズサービスの計画問題の解決を簡素化するための 1 つ以上の ソルバー インスタンスのファサードです。

Solver.solve (...) メソッドとは異なり、SolverManager は、次の特性があります。

  • SolverManager.solve (…) はすぐに戻ります。呼び出し元のスレッドをブロックせずに、非同期解決の問題をスケジュールします。これにより、HTTP およびその他のテクノロジーのタイムアウトの問題が回避されます。
  • SolverManager.solve (…) は、同じドメインの複数の計画問題を並行して解決します。

内部的には、 SolverManager は、Solver.solve (…) を呼び出すソルバースレッドのスレッドプールと、最適なソリューション変更イベントを処理するコンシューマースレッドのスレッドプールを管理します。

Quarkus と SpringBoot では、 SolverManager インスタンスがコードに自動的に挿入されます。Quarkus または SpringBoot 以外のプラットフォームを使用している場合は、 create (…) メソッドを使用して SolverManager インスタンスをビルドします。

SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../cloudBalancingSolverConfig.xml");
SolverManager<CloudBalance, UUID> solverManager = SolverManager.create(solverConfig, new SolverManagerConfig());

SolverManager.solve (…) メソッドに送信される各問題には、一意の問題 ID が必要です。後で getSolverStatus (problemId) または terminateEarly (problemId) を呼び出すと、その問題 ID を使用して計画の問題を区別します。問題 ID は、LongStringjava.util.UUID などのイミュータブルなクラスである必要があります。

SolverManagerConfig クラスには、並行して実行されるソルバーの数を制御する parallelSolverCount プロパティーがあります。たとえば、 parallelSolverCount プロパティー `が 4 に設定されていて、5 つの問題を送信すると、4 つの問題がすぐに解決を開始し、最初の問題の 1 つが終了すると 5 番目の問題が開始します。これらの問題がそれぞれ 5 分間解決した場合、5 番目の問題は 10 分で終了します。デフォルトでは、 parallelSolverCountAUTO に設定されており、ソルバーの moveThreadCount に関係なく、CPU コアの半分に解決されます。

最適なソリューションを取得するには、終了を解決した後、通常は SolverJob.getFinalBestSolution() を使用します。

CloudBalance problem1 = ...;
UUID problemId = UUID.randomUUID();
// Returns immediately
SolverJob<CloudBalance, UUID> solverJob = solverManager.solve(problemId, problem1);
...
CloudBalance solution1;
try {
    // Returns only after solving terminates
    solution1 = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
    throw ...;
}

ただし、ユーザーがソリューションを必要とする前にバッチの問題を解決する方法と、ユーザーがソリューションを積極的に待っている間にライブで解決する方法の両方について、より良いアプローチがあります。

現在の SolverManager 実装は単一のコンピューターノードで実行されますが、将来の作業は、クラウド全体にソルバーの負荷を分散することを目的としています。

9.1. 問題のバッチ解決

バッチ解決とは、複数のデータセットを並行して解決することです。バッチ解決は夜間処理で特に役立ちます。

  • 通常、深夜には問題の変化はほとんどまたはまったくありません。一部の組織は期限を強制します。たとえば、深夜零時までに休日のリクエストを送信するといったものです。
  • ソルバーは、結果を待つ人が誰もいないため、CPU リソースが安価であることが多いため、はるかに長く、多くの場合は数時間実行できます。
  • 翌営業日に従業員が職場に到着したときにソリューションが利用できます。

手順

parallelSolverCount によって制限した上で並列バッチでの問題解決するには、各データセットのためには、以下のクラスによって作成した以下の各データについて solve (...) を呼び出します。

public class TimeTableService {

    private SolverManager<TimeTable, Long> solverManager;

    // Returns immediately, call it for every data set
    public void solveBatch(Long timeTableId) {
        solverManager.solve(timeTableId,
                // Called once, when solving starts
                this::findById,
                // Called once, when solving ends
                this::save);
    }

    public TimeTable findById(Long timeTableId) {...}

    public void save(TimeTable timeTable) {...}

}

9.2. 解決して進捗状況を確認する

ユーザーがソリューションを待っている間にソルバーが実行されている場合、ユーザーは結果を受け取るまでに数分または数時間待つ必要がある場合があります。すべてが順調に進んでいることをユーザーに保証するために、これまでに達成された最良の解決策と最良のスコアを表示して進捗状況を示します。

手順

  1. 中間の最良のソリューションを処理するには、solveAndListen(…) を 使用します。

    public class TimeTableService {
    
        private SolverManager<TimeTable, Long> solverManager;
    
        // Returns immediately
        public void solveLive(Long timeTableId) {
            solverManager.solveAndListen(timeTableId,
                    // Called once, when solving starts
                    this::findById,
                    // Called multiple times, for every best solution change
                    this::save);
        }
    
        public TimeTable findById(Long timeTableId) {...}
    
        public void save(TimeTable timeTable) {...}
    
        public void stopSolving(Long timeTableId) {
            solverManager.terminateEarly(timeTableId);
        }
    
    }

    この実装は、データベースを使用して、データベースをポーリングする UI と通信します。より高度な実装は、最適なソリューションを UI またはメッセージングキューに直接プッシュします。

  2. ユーザーが中間の最良の解決策に満足し、より良い解決策をこれ以上待ちたくない場合は、 SolverManager.terminateEarly (problemId) を呼び出します。

パート IV. Red Hat build of OptaPlanner クイックスタートガイド

以下の手順に従って、従業員の勤務表サンプルに、ShiftAssignment データオブジェクトをプランニングエンティティーとして定義します。

  • Red Hat build of Quarkus プラットフォーム上の Red Hat build of OptaPlanner: 時間割のクイックスタートガイド
  • Red Hat build of Quarkus プラットフォーム上の Red Hat build of OptaPlanner: ワクチン接種予約スケジューラーのクイックスタートガイド
  • Red Hat build of Quarkus プラットフォーム上の Red Hat build of OptaPlanner: 従業員スケジューラーのクイックスタートガイド
  • Spring Boot 上の Red Hat build of OptaPlanner: 時間割のクイックスタートガイド
  • Java Solver を使用した Red Hat build of OptaPlanner: 時間割のクイックスタートガイド

第10章 Red Hat build of Quarkus プラットフォーム上の Red Hat build of OptaPlanner: 時間割のクイックスタートガイド

本書では、Red Hat build of OptaPlanner の制約解決人工知能 (AI) を使用して Red Hat build of Quarkus アプリケーションを作成するプロセスを説明します。学生および教師向けの時間割を最適化する REST アプリケーションを構築していきます。

学校の時間割解の図

サービスは、AI を使用して、以下のハードおよびソフトの スケジュール制約 に準拠し、Lesson インスタンスを Timeslot インスタンスと Room インスタンスに自動的に割り当てます。

  • 1 部屋に同時に割り当てることができる授業は、最大 1 コマです。
  • 教師が同時に一度に行うことができる授業は最大 1 回です。
  • 生徒は同時に出席できる授業は最大 1 コマです。
  • 教師は、1 つの部屋での授業を希望します。
  • 教師は、連続した授業を好み、授業間に時間が空くのを嫌います。

数学的に考えると、学校の時間割は NP 困難 の問題であります。つまり、スケーリングが困難です。総当たり攻撃で考えられる組み合わせを単純にすべて反復すると、スーパーコンピューターを使用したとしても、非自明的なデータセットを取得するのに数百年かかります。幸い、Red Hat build of OptaPlanner などの AI 制約ソルバーには、妥当な時間内にほぼ最適なソリューションを提供する高度なアルゴリズムがあります。妥当な期間として考慮される内容は、問題の目的によって異なります。

前提条件

10.1. ドメインオブジェクトのモデル化

Red Hat build of OptaPlanner の時間割プロジェクトの目標は、レッスンごとに時間枠と部屋に割り当てることです。これには、次の図に示すように、TimeslotLesson、および Room の 3 つのクラスを追加します。

タイムスロットを示す時間割クラス図

Timeslot

Timeslot クラスは、Monday 10:30 - 11:30Tuesday 13:30 - 14:30 など、授業の長さを表します。この例では、時間枠はすべて同じ長さ (期間) で、昼休みまたは他の休憩時間にはこのスロットはありません。

高校のスケジュールは毎週 (同じ内容が) 繰り返されるだけなので、時間枠には日付がありません。また、継続的プランニング は必要ありません。解決時に Timeslot インスタンスが変更しないため、Timeslot は 問題ファクト と呼ばれます。このようなクラスには OptaPlanner 固有のアノテーションは必要ありません。

Room

Room クラスは、Room ARoom B など、授業の場所を表します。以下の例では、どの部屋も定員制限がなく、すべての授業に対応できます。

Room インスタンスは解決時に変化しないため、Room問題ファクト でもあります。

Lesson

授業中 (Lesson クラスで表現)、教師は複数の生徒に Math by A.Turing for 9th gradeChemistry by M.Curie for 10th grade などの教科を指導します。ある教科について、毎週複数回、同じ教師が同じ生徒グループを指導する場合は、Lesson インスタンスが複数使用されますが、それらは id で識別可能です。たとえば、9 年生の場合は、1 週間に 6 回数学の授業があります。

解決中に、OptaPlanner は、Lesson クラスの timeslot フィールドと room フィールドを変更して、各授業を、時間枠 1 つ、部屋 1 つに割り当てます。OptaPlanner はこれらのフィールドを変更するため、Lessonプランニングエンティティー となります。

計画変数と対話する Timeslot クラスと Room クラスを示す Timetable クラス図

前図では、オレンジのフィールド以外のほぼすべてのフィールドに、入力データが含まれています。授業の timeslot フィールドと room フィールドは、入力データに割り当てられておらず (null)、出力データに割り当てられて (null ではない) います。Red Hat build of OptaPlanner は、解決時にこれらのフィールドを変更します。このようなフィールドはプランニング変数と呼ばれます。このフィールドを OptaPlanner に認識させるには、timeslot フィールドと room のフィールドに @PlanningVariable アノテーションが必要です。このフィールドに含まれる Lesson クラスには、@PlanningEntity アノテーションが必要です。

手順

  1. src/main/java/com/example/domain/Timeslot.java クラスを作成します。

    package com.example.domain;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    
    public class Timeslot {
    
        private DayOfWeek dayOfWeek;
        private LocalTime startTime;
        private LocalTime endTime;
    
        private Timeslot() {
        }
    
        public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
            this.dayOfWeek = dayOfWeek;
            this.startTime = startTime;
            this.endTime = endTime;
        }
    
        @Override
        public String toString() {
            return dayOfWeek + " " + startTime.toString();
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public DayOfWeek getDayOfWeek() {
            return dayOfWeek;
        }
    
        public LocalTime getStartTime() {
            return startTime;
        }
    
        public LocalTime getEndTime() {
            return endTime;
        }
    
    }

    後述しているように、toString() メソッドで出力を短くするため、OptaPlanner の DEBUG ログまたは TRACE ログの読み取りが簡単になっています。

  2. src/main/java/com/example/domain/Room.java クラスを作成します。

    package com.example.domain;
    
    public class Room {
    
        private String name;
    
        private Room() {
        }
    
        public Room(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public String getName() {
            return name;
        }
    
    }
  3. src/main/java/com/example/domain/Lesson.java クラスを作成します。

    package com.example.domain;
    
    import org.optaplanner.core.api.domain.entity.PlanningEntity;
    import org.optaplanner.core.api.domain.variable.PlanningVariable;
    
    @PlanningEntity
    public class Lesson {
    
        private Long id;
    
        private String subject;
        private String teacher;
        private String studentGroup;
    
        @PlanningVariable(valueRangeProviderRefs = "timeslotRange")
        private Timeslot timeslot;
    
        @PlanningVariable(valueRangeProviderRefs = "roomRange")
        private Room room;
    
        private Lesson() {
        }
    
        public Lesson(Long id, String subject, String teacher, String studentGroup) {
            this.id = id;
            this.subject = subject;
            this.teacher = teacher;
            this.studentGroup = studentGroup;
        }
    
        @Override
        public String toString() {
            return subject + "(" + id + ")";
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public Long getId() {
            return id;
        }
    
        public String getSubject() {
            return subject;
        }
    
        public String getTeacher() {
            return teacher;
        }
    
        public String getStudentGroup() {
            return studentGroup;
        }
    
        public Timeslot getTimeslot() {
            return timeslot;
        }
    
        public void setTimeslot(Timeslot timeslot) {
            this.timeslot = timeslot;
        }
    
        public Room getRoom() {
            return room;
        }
    
        public void setRoom(Room room) {
            this.room = room;
        }
    
    }

    Lesson クラスには @PlanningEntity アノテーションが含まれており、その中にプランニング変数が 1 つ以上含まれているため、OptaPlanner はこのクラスが解決時に変化することを認識します。

    timeslot フィールドには @PlanningVariable アノテーションがあるため、OptaPlanner は、このフィールドの値が変化することを認識しています。このフィールドに割り当てることのできる Timeslot インスタンスを見つけ出すために、OptaPlanner は valueRangeProviderRefs プロパティーを使用して値の範囲プロバイダーと連携し、List<Timeslot> を提供して選択できるようにします。値の範囲プロバイダーに関する詳細は、「プランニングソリューションでのドメインオブジェクトの収集」 を参照してください。

    room フィールドにも、同じ理由で @PlanningVariable アノテーションが含まれます。

10.2. 制約の定義およびスコアの計算

問題の解決時に スコア で導かれた解の質を表します。スコアが高いほど質が高くなります。Red Hat build of OptaPlanner は、利用可能な時間内で見つかった解の中から最高スコアのものを探し出します。これが 最適 解である可能性があります。

時間割の例のユースケースでは、ハードとソフト制約を使用しているため、HardSoftScore クラスでスコアを表します。

  • ハード制約は、絶対に違反しないでください。たとえば、部屋に同時に割り当てることができる授業は、最大 1 コマです。
  • ソフト制約は、違反しないようにしてください。たとえば、教師は、1 つの部屋での授業を希望します。

ハード制約は、他のハード制約と比べて、重み付けを行います。ソフト制約は、他のソフト制約と比べて、重み付けを行います。ハード制約は、それぞれの重みに関係なく、常にソフト制約よりも高くなります。

EasyScoreCalculator クラスを実装して、スコアを計算できます。

public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> {

    @Override
    public HardSoftScore calculateScore(TimeTable timeTable) {
        List<Lesson> lessonList = timeTable.getLessonList();
        int hardScore = 0;
        for (Lesson a : lessonList) {
            for (Lesson b : lessonList) {
                if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot())
                        && a.getId() < b.getId()) {
                    // A room can accommodate at most one lesson at the same time.
                    if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) {
                        hardScore--;
                    }
                    // A teacher can teach at most one lesson at the same time.
                    if (a.getTeacher().equals(b.getTeacher())) {
                        hardScore--;
                    }
                    // A student can attend at most one lesson at the same time.
                    if (a.getStudentGroup().equals(b.getStudentGroup())) {
                        hardScore--;
                    }
                }
            }
        }
        int softScore = 0;
        // Soft constraints are only implemented in the "complete" implementation
        return HardSoftScore.of(hardScore, softScore);
    }

}

残念ながら、この解は漸増的ではないので、適切にスケーリングされません。授業が別の時間枠や教室に割り当てられるたびに、全授業が再評価され、新しいスコアが計算されます。

src/main/java/com/example/solver/TimeTableConstraintProvider.java クラスを作成して、漸増的スコア計算を実行すると、解がより優れたものになります。このクラスは、Java 8 Streams と SQL を基にした OptaPlanner の ConstraintStream API を使用します。ConstraintProvider は、EasyScoreCalculator と比べ、スケーリングの規模が遥かに大きくなっています (O (n²) ではなく O (n))。

手順

以下の src/main/java/com/example/solver/TimeTableConstraintProvider.java クラスを作成します。

package com.example.solver;

import com.example.domain.Lesson;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaplanner.core.api.score.stream.Joiners;

public class TimeTableConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                // Hard constraints
                roomConflict(constraintFactory),
                teacherConflict(constraintFactory),
                studentGroupConflict(constraintFactory),
                // Soft constraints are only implemented in the "complete" implementation
        };
    }

    private Constraint roomConflict(ConstraintFactory constraintFactory) {
        // A room can accommodate at most one lesson at the same time.

        // Select a lesson ...
        return constraintFactory.forEach(Lesson.class)
                // ... and pair it with another lesson ...
                .join(Lesson.class,
                        // ... in the same timeslot ...
                        Joiners.equal(Lesson::getTimeslot),
                        // ... in the same room ...
                        Joiners.equal(Lesson::getRoom),
                        // ... and the pair is unique (different id, no reverse pairs)
                        Joiners.lessThan(Lesson::getId))
                // then penalize each pair with a hard weight.
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("Room conflict");
    }

    private Constraint teacherConflict(ConstraintFactory constraintFactory) {
        // A teacher can teach at most one lesson at the same time.
        return constraintFactory.forEach(Lesson.class)
                .join(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getTeacher),
                        Joiners.lessThan(Lesson::getId))
                        .penalize(HardSoftScore.ONE_HARD)
                        .asConstraint("Teacher conflict");
    }

    private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
        // A student can attend at most one lesson at the same time.
        return constraintFactory.forEach(Lesson.class)
                .join(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getStudentGroup),
                        Joiners.lessThan(Lesson::getId))
                        .penalize(HardSoftScore.ONE_HARD)
                        .asConstraint("Student group conflict");
    }

}

10.3. プランニングソリューションでのドメインオブジェクトの収集

TimeTable インスタンスは、単一データセットの Timeslot インスタンス、Room インスタンス、および Lesson インスタンスをラップします。さらに、このインスタンスは、特定のプランニング変数の状態を持つ授業がすべて含まれているため、このインスタンスは プランニングソリューション となり、スコアが割り当てられます。

  • 授業がまだ割り当てられていない場合は、スコアが -4init/0hard/0soft のソリューションなど、初期化されていない ソリューションとなります。
  • ハード制約に違反する場合、スコアが -2hard/-3soft のソリューションなど、実行不可 なソリューションとなります。
  • 全ハード制約に準拠している場合は、スコアが 0hard/-7soft など、実行可能 なソリューションとなります。

TimeTable クラスには @PlanningSolution アノテーションが含まれているため、Red Hat build of OptaPlanner はこのクラスに全入出力データが含まれていることを認識します。

具体的には、このクラスは問題の入力です。

  • 全時間枠が含まれる timeslotList フィールド

    • これは、解決時に変更されないため、問題ファクトリーストです。
  • 全部屋が含まれる roomList フィールド

    • これは、解決時に変更されないため、問題ファクトリーストです。
  • 全授業が含まれる lessonList フィールド

    • これは、解決時に変更されるため、プランニングエンティティーです。
    • Lesson:

      • timeslot フィールドおよび room フィールドの値は通常、null で未割り当てです。これらの値は、プランニング変数です。
      • subjectteacherstudentGroup などの他のフィールドは入力されます。これらのフィールドは問題プロパティーです。

ただし、このクラスはソリューションの出力でもあります。

  • Lesson インスタンスごとの lessonList フィールドには、解決後は null ではない timeslot フィールドと room フィールドが含まれます。
  • 出力ソリューションの品質を表す score フィールド (例: 0hard/-5soft)

手順

src/main/java/com/example/domain/TimeTable.java クラスを作成します。

package com.example.domain;

import java.util.List;

import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;

@PlanningSolution
public class TimeTable {

    @ValueRangeProvider(id = "timeslotRange")
    @ProblemFactCollectionProperty
    private List<Timeslot> timeslotList;

    @ValueRangeProvider(id = "roomRange")
    @ProblemFactCollectionProperty
    private List<Room> roomList;

    @PlanningEntityCollectionProperty
    private List<Lesson> lessonList;

    @PlanningScore
    private HardSoftScore score;

    private TimeTable() {
    }

    public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
            List<Lesson> lessonList) {
        this.timeslotList = timeslotList;
        this.roomList = roomList;
        this.lessonList = lessonList;
    }

    // ********************************
    // Getters and setters
    // ********************************

    public List<Timeslot> getTimeslotList() {
        return timeslotList;
    }

    public List<Room> getRoomList() {
        return roomList;
    }

    public List<Lesson> getLessonList() {
        return lessonList;
    }

    public HardSoftScore getScore() {
        return score;
    }

}

値の範囲のプロバイダー

timeslotList フィールドは、値の範囲プロバイダーです。これは Timeslot インスタンスを保持し、OptaPlanner がこのインスタンスを選択して、Lesson インスタンスの timeslot フィールドに割り当てることができます。timeslotList フィールドには @ValueRangeProvider アノテーションがあり、id を、Lesson@PlanningVariablevalueRangeProviderRefs に一致させます。

同じロジックに従い、roomList フィールドにも @ValueRangeProvider アノテーションが含まれています。

問題ファクトとプランニングエンティティーのプロパティー

さらに OptaPlanner は、変更可能な Lesson インスタンス、さらに TimeTableConstraintProvider によるスコア計算に使用する Timeslot インスタンスと Room インスタンスを取得する方法を把握しておく必要があります。

timeslotList フィールドと roomList フィールドには @ProblemFactCollectionProperty アノテーションが含まれているため、TimeTableConstraintProvider はこれらのインスタンスから選択できます。

lessonList には @PlanningEntityCollectionProperty アノテーションが含まれているため、OptaPlanner は解決時に変更でき、TimeTableConstraintProvider はこの中から選択できます。

10.4. Solver サービスの作成

REST スレッドで計画問題を解決すると、HTTP タイムアウトの問題が発生します。そのため、Quarkus スターターでは SolverManager を注入することで、個別のスレッドプールでソルバーを実行して複数のデータセットを並行して解決できます。

手順

src/main/java/org/acme/optaplanner/rest/TimeTableResource.java クラスを作成します。

package org.acme.optaplanner.rest;

import java.util.UUID;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

import org.acme.optaplanner.domain.TimeTable;
import org.optaplanner.core.api.solver.SolverJob;
import org.optaplanner.core.api.solver.SolverManager;

@Path("/timeTable")
public class TimeTableResource {

    @Inject
    SolverManager<TimeTable, UUID> solverManager;

    @POST
    @Path("/solve")
    public TimeTable solve(TimeTable problem) {
        UUID problemId = UUID.randomUUID();
        // Submit the problem to start solving
        SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
        TimeTable solution;
        try {
            // Wait until the solving ends
            solution = solverJob.getFinalBestSolution();
        } catch (InterruptedException | ExecutionException e) {
            throw new IllegalStateException("Solving failed.", e);
        }
        return solution;
    }

}

この例では、初期実装はソルバーが完了するのを待つため、HTTP タイムアウトがまだ発生します。complete 実装を使用することで、より適切に HTTP タイムアウトを回避できます。

10.5. ソルバー終了時間の設定

プランニングアプリケーションに終了設定または終了イベントがない場合、理論的には永続的に実行されることになり、実際には HTTP タイムアウトエラーが発生します。これが発生しないようにするには、optaplanner.solver.termination.spent-limit パラメーターを使用して、アプリケーションが終了してからの時間を指定します。多くのアプリケーションでは、この時間を最低でも 5 分 (5m) に設定します。ただし、時間割の例では、解決時間を 5 分に制限すると、期間が十分に短いため HTTP タイムアウトを回避できます。

手順

以下の内容を含む src/main/resources/application.properties ファイルを作成します。

quarkus.optaplanner.solver.termination.spent-limit=5s

10.6. 時間割アプリケーションの実行

時間割プロジェクトを作成したら、開発モードで実行します。開発モードでは、アプリケーションの実行中にアプリケーションソースおよび設定を更新できます。変更が実行中のアプリケーションに反映されます。

前提条件

  • 時間割プロジェクトを作成している。

手順

  1. 開発モードでアプリケーションをコンパイルするには、プロジェクトディレクトリーから以下のコマンドを入力します。

    ./mvnw compile quarkus:dev
  2. REST サービスをテストします。任意の REST クライアントを使用できます。この例では Linux curl コマンドを使用して POST 要求を送信します。

    $ curl -i -X POST http://localhost:8080/timeTable/solve -H "Content-Type:application/json" -d '{"timeslotList":[{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"}],"roomList":[{"name":"Room A"},{"name":"Room B"}],"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade"},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade"},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade"},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade"}]}'

    application.properties ファイルで定義した 終了時間 で指定した期間後に、サービスにより、以下の例のような出力が返されます。

    HTTP/1.1 200
    Content-Type: application/json
    ...
    
    {"timeslotList":...,"roomList":...,"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room A"}},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room A"}},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room B"}},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room B"}}],"score":"0hard/0soft"}

    アプリケーションにより、4 つの授業がすべて 2 つの時間枠、そして 2 つある部屋のうちの 1 つに割り当てられている点に注目してください。また、すべてのハード制約に準拠することに注意してください。たとえば、M. Curie の 2 つの授業は異なる時間スロットにあります。

  3. 解決時間の OptaPlanner の実行内容を確認するには、サーバー側で情報ログを確認します。以下は、情報ログ出力の例です。

    ... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
    ... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4).
    ... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398).
    ... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE).

10.7. アプリケーションのテスト

適切なアプリケーションにはテストが含まれます。timetable プロジェクトで制約とソルバーをテストします。

10.7.1. 学校の時間割の制約をテストする

timetable プロジェクトの各制約を個別にテストするには、単体テストで ConstraintVerifier を使用します。これにより、各制約のコーナーケースが他のテストから分離されてテストされるため、適切なテストカバレッジで新しい制約を追加する際のメンテナンスが軽減されます。

このテストは、制約 TimeTableConstraintProvider::roomConflict が、同じ部屋で 3 つのレッスンを与えられ、そのうちの 2 つのレッスンが同じタイムスロットを持つ場合、一致の重み 1 でペナルティを課すことを検証します。したがって、制約の重みが 10hard の場合、スコアは -10hard 減少します。

手順

src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java クラスを作成します。

package org.acme.optaplanner.solver;

import java.time.DayOfWeek;
import java.time.LocalTime;

import javax.inject.Inject;

import io.quarkus.test.junit.QuarkusTest;
import org.acme.optaplanner.domain.Lesson;
import org.acme.optaplanner.domain.Room;
import org.acme.optaplanner.domain.TimeTable;
import org.acme.optaplanner.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;

@QuarkusTest
class TimeTableConstraintProviderTest {

    private static final Room ROOM = new Room("Room1");
    private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON);
    private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON);

    @Inject
    ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier;

    @Test
    void roomConflict() {
        Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1");
        Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2");
        Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3");

        firstLesson.setRoom(ROOM);
        firstLesson.setTimeslot(TIMESLOT1);

        conflictingLesson.setRoom(ROOM);
        conflictingLesson.setTimeslot(TIMESLOT1);

        nonConflictingLesson.setRoom(ROOM);
        nonConflictingLesson.setTimeslot(TIMESLOT2);

        constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
                .given(firstLesson, conflictingLesson, nonConflictingLesson)
                .penalizesBy(1);
    }

}

制約の重みが ConstraintProvider でハードコーディングされている場合でも、ConstraintVerifier がテスト中に制約の重みを無視することに注意してください。これは、実稼動に入る前に制約の重みが定期的に変更されるためです。このように、制約の重みの微調整によって単体テストが中断されることはありません。

10.7.2. 学校の時間割ソルバーをテストする

以下の例では、Red Hat build of Quarkus で Red Hat build of OptaPlanner の時間割プロジェクトをテストします。このアプリケーションは、JUnit テストを使用してテストのデータセットを生成し、TimeTableController に送信して解決します。

手順

  1. 以下の内容を含む src/test/java/com/example/rest/TimeTableResourceTest.java クラスを作成します。

    package com.exmaple.optaplanner.rest;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.inject.Inject;
    
    import io.quarkus.test.junit.QuarkusTest;
    import com.exmaple.optaplanner.domain.Room;
    import com.exmaple.optaplanner.domain.Timeslot;
    import com.exmaple.optaplanner.domain.Lesson;
    import com.exmaple.optaplanner.domain.TimeTable;
    import com.exmaple.optaplanner.rest.TimeTableResource;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @QuarkusTest
    public class TimeTableResourceTest {
    
        @Inject
        TimeTableResource timeTableResource;
    
        @Test
        @Timeout(600_000)
        public void solve() {
            TimeTable problem = generateProblem();
            TimeTable solution = timeTableResource.solve(problem);
            assertFalse(solution.getLessonList().isEmpty());
            for (Lesson lesson : solution.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(solution.getScore().isFeasible());
        }
    
        private TimeTable generateProblem() {
            List<Timeslot> timeslotList = new ArrayList<>();
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));
    
            List<Room> roomList = new ArrayList<>();
            roomList.add(new Room("Room A"));
            roomList.add(new Room("Room B"));
            roomList.add(new Room("Room C"));
    
            List<Lesson> lessonList = new ArrayList<>();
            lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade"));
            lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade"));
            lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade"));
            lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade"));
            lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade"));
    
            lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade"));
            lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade"));
            lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade"));
            lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade"));
            lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade"));
            return new TimeTable(timeslotList, roomList, lessonList);
        }
    
    }

    このテストは、解決後にすべての授業がタイムスロットと部屋に割り当てられていることを確認します。また、実行可能解 (ハード制約の違反なし) も確認します。

  2. テストプロパティーを src / main / resources /application.properties ファイルに追加します。

    # The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
    # It's recommended to run for at least 5 minutes ("5m") otherwise.
    quarkus.optaplanner.solver.termination.spent-limit=5s
    
    # Effectively disable this termination in favor of the best-score-limit
    %test.quarkus.optaplanner.solver.termination.spent-limit=1h
    %test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*soft

通常、ソルバーは 200 ミリ秒未満で実行可能解を検索します。application.properties が、実行可能なソリューション (0hard/*soft) が見つかると同時に終了するように、テスト中のソルバーの終了を上書きします。こうすることで、ユニットテストが任意のハードウェアで実行される可能性があるため、ソルバーの時間をハードコード化するのを回避します。このアプローチを使用することで、動きが遅いシステムであっても、実行可能なソリューションを検索するのに十分な時間だけテストが実行されます。ただし、高速システムでも、厳密に必要とされる時間よりもミリ秒単位で長く実行されることはありません。

10.8. ロギング

Red Hat build of OptaPlanner の時間割プロジェクトを完了後にロギング情報を使用すると、ConstraintProvider で制約が微調整しやすくなります。info ログファイルでスコア計算の速度を確認して、制約に加えた変更の影響を評価します。デバッグモードでアプリケーションを実行して、アプリケーションが行う手順をすべて表示するか、追跡ログを使用して全手順および動きをロギングします。

手順

  1. 時間割アプリケーションを一定の時間 (例: 5 分) 実行します。
  2. 以下の例のように、log ファイルのスコア計算の速度を確認します。

    ... Solving ended: ..., score calculation speed (29455/sec), ...
  3. 制約を変更して、同じ時間、プランニングアプリケーションを実行し、log ファイルに記録されているスコア計算速度を確認します。
  4. アプリケーションをデバッグモードで実行して、アプリケーションの全実行ステップをログに記録します。

    • コマンドラインからデバッグモードを実行するには、-D システムプロパティーを使用します。
    • デバッグモードを永続的に有効にするには、以下の行を application.properties ファイルに追加します。

      quarkus.log.category."org.optaplanner".level=debug

      以下の例では、デバッグモードでの log ファイルの出力を表示します。

      ... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
      ...     CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]).
      ...     CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]).
      ...
  5. trace ロギングを使用して、全手順、および手順ごとの全動きを表示します。

10.9. データベースを Quarkus OptaPlanner 学校の時間割アプリケーションと統合する

Quarkus OptaPlanner 学校の時間割アプリケーションを作成したら、それをデータベースと統合し、ウェブベースのユーザーインターフェイスを作成して時間割を表示できます。

前提条件

  • Quarkus OptaPlanner 学校の時間割アプリケーションがあります。

手順

  1. Hibernate と Panache を使用して、TimeslotRoom、および Lesson インスタンスをデータベースに格納します。詳細については、Panache を使用した単純化された Hibernate ORM を参照してください。
  2. REST を介してインスタンスを公開します。詳細については、JSON REST サービスの記述 を参照してください。
  3. TimeTableResource クラスを更新して、単一のトランザクションで TimeTable インスタンスの読み取りと書き込みを行います。

    package org.acme.optaplanner.rest;
    
    import javax.inject.Inject;
    import javax.transaction.Transactional;
    import javax.ws.rs.GET;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    
    import io.quarkus.panache.common.Sort;
    import org.acme.optaplanner.domain.Lesson;
    import org.acme.optaplanner.domain.Room;
    import org.acme.optaplanner.domain.TimeTable;
    import org.acme.optaplanner.domain.Timeslot;
    import org.optaplanner.core.api.score.ScoreManager;
    import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
    import org.optaplanner.core.api.solver.SolverManager;
    import org.optaplanner.core.api.solver.SolverStatus;
    
    @Path("/timeTable")
    public class TimeTableResource {
    
        public static final Long SINGLETON_TIME_TABLE_ID = 1L;
    
        @Inject
        SolverManager<TimeTable, Long> solverManager;
        @Inject
        ScoreManager<TimeTable, HardSoftScore> scoreManager;
    
        // To try, open http://localhost:8080/timeTable
        @GET
        public TimeTable getTimeTable() {
            // Get the solver status before loading the solution
            // to avoid the race condition that the solver terminates between them
            SolverStatus solverStatus = getSolverStatus();
            TimeTable solution = findById(SINGLETON_TIME_TABLE_ID);
            scoreManager.updateScore(solution); // Sets the score
            solution.setSolverStatus(solverStatus);
            return solution;
        }
    
        @POST
        @Path("/solve")
        public void solve() {
            solverManager.solveAndListen(SINGLETON_TIME_TABLE_ID,
                    this::findById,
                    this::save);
        }
    
        public SolverStatus getSolverStatus() {
            return solverManager.getSolverStatus(SINGLETON_TIME_TABLE_ID);
        }
    
        @POST
        @Path("/stopSolving")
        public void stopSolving() {
            solverManager.terminateEarly(SINGLETON_TIME_TABLE_ID);
        }
    
        @Transactional
        protected TimeTable findById(Long id) {
            if (!SINGLETON_TIME_TABLE_ID.equals(id)) {
                throw new IllegalStateException("There is no timeTable with id (" + id + ").");
            }
            // Occurs in a single transaction, so each initialized lesson references the same timeslot/room instance
            // that is contained by the timeTable's timeslotList/roomList.
            return new TimeTable(
                    Timeslot.listAll(Sort.by("dayOfWeek").and("startTime").and("endTime").and("id")),
                    Room.listAll(Sort.by("name").and("id")),
                    Lesson.listAll(Sort.by("subject").and("teacher").and("studentGroup").and("id")));
        }
    
        @Transactional
        protected void save(TimeTable timeTable) {
            for (Lesson lesson : timeTable.getLessonList()) {
                // TODO this is awfully naive: optimistic locking causes issues if called by the SolverManager
                Lesson attachedLesson = Lesson.findById(lesson.getId());
                attachedLesson.setTimeslot(lesson.getTimeslot());
                attachedLesson.setRoom(lesson.getRoom());
            }
        }
    
    }

    この例には TimeTable インスタンスが含まれています。ただし、マルチテナンシーを有効にして、複数の学校の TimeTable インスタンスを並行して処理できます。

    getTimeTable() メソッドは、データベースから最新の時間割を返します。自動的に挿入される ScoreManager メソッドを使用して、そのタイムテーブルのスコアを計算し、UI で使用できるようにします。

    solve() メソッドは、ジョブを開始して、現在の時間割を解決し、時間枠と部屋の割り当てをデータベースに保存します。このメソッドは、SolverManager.solveAndListen() メソッドを使用して、中間の最適解をリッスンし、それに合わせてデータベースを更新します。バックエンドがまだ解決している間、UI はこれを使用して進行状況を表示します。

  4. TimeTableResourceTest クラスを更新して、solve() メソッドがすぐに戻ることを反映し、ソルバーが解決を完了するまで最新の解をポーリングするようにします。

    package org.acme.optaplanner.rest;
    
    import javax.inject.Inject;
    
    import io.quarkus.test.junit.QuarkusTest;
    import org.acme.optaplanner.domain.Lesson;
    import org.acme.optaplanner.domain.TimeTable;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    import org.optaplanner.core.api.solver.SolverStatus;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @QuarkusTest
    public class TimeTableResourceTest {
    
        @Inject
        TimeTableResource timeTableResource;
    
        @Test
        @Timeout(600_000)
        public void solveDemoDataUntilFeasible() throws InterruptedException {
            timeTableResource.solve();
            TimeTable timeTable = timeTableResource.getTimeTable();
            while (timeTable.getSolverStatus() != SolverStatus.NOT_SOLVING) {
                // Quick polling (not a Test Thread Sleep anti-pattern)
                // Test is still fast on fast machines and doesn't randomly fail on slow machines.
                Thread.sleep(20L);
                timeTable = timeTableResource.getTimeTable();
            }
            assertFalse(timeTable.getLessonList().isEmpty());
            for (Lesson lesson : timeTable.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(timeTable.getScore().isFeasible());
        }
    
    }
  5. これらの REST メソッドの上に Web UI を構築して、タイムテーブルを視覚的に表現します。
  6. クイックスタートソースコード を確認します。

10.10. Micrometer と Prometheus を使用して学校の時間割を監視する OptaPlanner Quarkus アプリケーション

OptaPlanner は、Java アプリケーション用のメトリック計測ライブラリーである Micrometer を介してメトリックを公開します。Prometheus で Micrometer を使用して、学校の時間割アプリケーションで OptaPlanner ソルバーを監視できます。

前提条件

  • Quarkus OptaPlanner 学校の時間割アプリケーションを作成しました。
  • Prometheus がインストールされている。Prometheus のインストールについては、Prometheus の Web サイトを参照してください。

手順

  1. 学校の時間割 pom.xml ファイルに Micrometer Prometheus 依存関係を追加します。

    <dependency>
     <groupId>io.quarkus</groupId>
     <artifactId>quarkus-micrometer-registry-prometheus</artifactId>
    </dependency>
  2. 学校の時間割アプリケーションを開始します。

    mvn compile quarkus:dev
  3. Web ブラウザーで http://localhost:8080/q/metric を開きます。

第11章 Red Hat build of Quarkus プラットフォーム上の Red Hat build of OptaPlanner: ワクチン接種予約スケジューラーのクイックスタートガイド

OptaPlanner ワクチン接種予約スケジューラーのクイックスタートを使用して、効率性および公平性の高いワクチン接種スケジュールを開発できます。ワクチン接種予約スケジューラーは、人工知能 (AI) を使用して摂取者の優先順位を決定し、複数の制約や優先順位に基づいて時間枠を割り当てます。

前提条件

11.1. OptaPlanner ワクチン接種予約のスケジューラーの仕組み

スケジュール予約には、主に 2 つの方法があります。システムは、ユーザーが予約枠 (ユーザーの選択) を選択できるようにするか、システムが予約枠を割り当ててそのユーザーにワクチン予約の日付と場所 (システムの自動割当) を通知することができます。OptaPlanner のワクチン接種予約スケジューラーは、システム自動割り当てのアプローチを使用します。OptaPlanner ワクチン接種予約スケジューラーでは、システムに情報を提供するアプリケーションを作成して、システムが予約を割り当てます。

このアプローチの特徴は次のとおりです。

  • 予約枠は優先順位に基づいて割り当てられます。
  • システムは、事前設定したプランニング制約に基づいて、最適な時間と場所を割り当てます。
  • システムは、多数のユーザーが少ない予約数に殺到しても、圧迫されることはありません。

このアプローチにより、プランニング制約を使用して各ユーザーのスコアを作成し、できるだけ多くの人がワクチン接種を受けられるようにする問題を解決します。スコアで、予約を取得するタイミングが決まります。スコアが高いほど、早い段階で受信できる可能性が高くなります。

11.1.1. Red Hat build of OptaPlanner ワクチン接種予約のスケジューラーの制約

Red Hat build of OptaPlanner ワクチン接種予約のスケジューラー制約は、ハード、中、またはソフトのいずれかです。

  • ハード制約は、絶対に違反できません。ハード制約に違反している場合には、プランは実行不可なので実行できません。

    • 容量: 場所、時間に関わらず、ワクチン量よりも多く予約を取らない。
    • ワクチン接種年齢上限: ワクチンに年齢の上限がある場合には、1 回目のワクチン接種時にワクチンの年齢の上限以上の方には投与しないようにする。年齢にあったワクチンタイプを投与するようにします。たとえば、75 歳の方には、年齢の上限が 65 歳のワクチンの予約を割り当てないようにします。
    • 必要なワクチンタイプ: 必要なワクチンタイプを使用する。たとえば、2 回目のワクチンは、1 回目と同じタイプを使用する必要があります。
    • 準備が整う日: 指定の日付以降にワクチンを投与する。たとえば、2 回目の投与を受ける場合に、特定のワクチンタイプを最短で接種が可能な推奨日時よりも前に投与しないでください (例: 1 回目の接種から 26 日後など)。
    • 期日: 指定された日付以前にワクチンを接種する。たとえば、2 回目の投与を受ける場合に、特定のワクチンタイプを接種可能な推奨最終日よりも前に投与してください (例: 1 回目の接種から 3 ヶ月後など)。
    • 移動の最大距離の制限: 最寄りのワクチンセンターグループに、各接種者を割り当てる。通常、これは 3 つのセンターのいずれかになります。この制限は、距離ではなく移動時間によって計算されるので、郊外に居住の方に比べ、都市部に居住する方は最大距離が短くなります。
  • 中程度の制約は、全員に予約を割り当てる空きがない場合に、どの接種希望者が予約できないかを決定します。これは、過剰制約プランニングと呼ばれます。

    • 2 回目のワクチン接種予約: 理想の日付が計画枠外になってしまう場合を除き、2 回目のワクチン接種予約を割り当てずに放置しないようにする。
    • 優先度評価に基づくスケジュール設定: 各接種者に優先度評価がある。これは、通常年齢ですが、医療関係者などの場合には優先度が高くなります。優先度が最も低い場合にのみ、予約を割り当てずに放置できます。次のワクチン接種のタイミングで考慮されます。2 回目の投与は優先度評価よりも優先されるので、この制約は以前の制約よりもソフトです。
  • ソフト制約は、違反しないようにしてください。

    • 希望のワクチンセンター: 接種者が希望するワクチンセンターがある場合は、そのセンターで予約を入れる。
    • 距離: 接種者が割り当てられたワクチンセンターまで移動する距離を最小限に抑える。
    • 理想の日付: できるだけ指定の日付に近い日にワクチンを投与する。たとえば、2 回目の投与を受ける場合に、特定のワクチンで理想の日付に投与します (例: 1 回目の接種から 28 日後など)。この制約は、距離制約よりもソフトで、理想の日付に近づけるために、遠方まで出向かせる必要がないようにします。
    • 優先度評価: 優先度評価の高い方から、予約枠で日付が近い順に割り当てる。この制約は、距離制約よりもソフトで、遠方まで出向かせる必要がないようにします。2 回目の投与は優先度評価よりも優先されるので、この制約も以前の制約よりもソフトです。

ハード制約は、他のハード制約と比べて、重み付けを行います。ソフト制約は、他のソフト制約と比べて、重み付けを行います。ただし、ハード制約は、常に中程度およびソフト制約よりも優先されます。ハード制約に違反している場合には、プランは実行できません。ただし、ハード制約に違反していない場合には、優先度を判定するためにソフト制約および中程度の制約が考慮されます。空きがある予約枠よりも希望者が多いので、優先付けが必要です。2 回目の投与予約は必ず先に割り当て、後ほどシステムに負荷がかからないようにバックログの作成は回避します。その後、優先度評価をもとに割り当てていきます。優先度評価年齢から評価を開始します。この評価では、若い人よりお年寄りが優先されます。その後、特定の優先度のグループには、200-300 ポイントなど、余分にポイントが追加されます。これはグループの優先順位によって異なります。たとえば、看護師は追加で 1000 ポイントを受け取る可能性があります。こうすることで、年齢の高い看護師は、若い看護師よりも優先され、若い看護師は看護師ではない人よりも優先されます。以下の表は、この概念を示しています。

表11.1 優先順位評価の表
年齢職業優先順位の評価

60

看護師

1060

33

看護師

1033

71

定年退職

71

52

オフィスワーカー

52

11.1.2. Red Hat build of OptaPlanner のソルバー

OptaPlanner のコアには Solver があり、エンジンは、問題データセットを取り、プランニングの制約および設定をオーバーレイします。問題データセットには、人、ワクチン、ワクチンセンターなどの情報すべてが含まれます。ソルバーは、さまざまなデータを組み合わせて機能し、最終的に特定のセンターでワクチン接種予約を割り当てる時に、最適な予約スケジュールを決定します。以下は、ソルバーで作成されたスケジュールを示しています。

ソルバーが作成したワクチン接種スケジュール

11.1.3. 継続プランニング

継続プランニングは、1 つ以上の今後のプランニング期間をまとめて管理して、そのプロセスを月単位、週単位、日単位、1 時間単位、またはそれよりも短い単位で繰り返す手法です。プランニング枠は、指定した間隔で増分して進められます。以下は、毎日更新される 2 週間のプランニング枠を示しています。

毎日更新される 2 週間の計画期間

2 週間計画枠を半分に分割します。1 週間目は公開状態で、2 週間目がドラフト状態にあります。プランニング枠の公開部分とドラフト部分の両方で、予約を割り当てます。ただし、プランニング枠の公開部分の方だけに、予約の通知が行きます。他の予約は、次の実行でまだ簡単に変更できます。こうすることで、ソルバーを次回実行すると、必要に応じて OptaPlanner が柔軟にドラフトの部分の予約を変更できます。たとえば、2 回目の投与の準備が月曜にできており、理想の日付が水曜の場合には、同じ週のドラフト予約を指定できることが証明できるのであれば、OptaPlanner は月曜に予約を割り当てる必要はありません。

プランニング枠のサイズは指定できますが、問題領域のサイズを認識しておいてください。問題領域は、スケジュールの作成に使用されるさまざまな要素すべてです。計画期間が長いと、問題領域が大きくなります。

11.1.4. 固定されたプランニングエンティティー

1 日単位で継続的にプランニングしている場合には、 2 週間の中ですでに割り当てられている予約枠があります。ダブルブッキングされないように、OptaPlanner は予約枠が割り当て済みと固定することができます。1 つ以上の特有の割当のアンカリングや、OptaPlanner が強制的に固定の割当を除外してスケジュールするのに、固定機能を使用します。予約など、固定されたプランニングエンティティーは、解決時には変更されません。

エンティティーが固定されているかどうかは、予約の状態により分かります。予約の状態には OpenInvitedAcceptedRejected または Rescheduled があります。

注記

OptaPlanner エンジンは、予約が固定されているかどうかのみに着目するので、クイックスタートのデモコードに予約の状態は直接表示されません。

スケジュール済みの予約に近い日付で、プランニングできるようにする必要があります。状態が Invited または Accepted の予約が固定されます。状態が OpenReschedule および Rejected の予約は固定さず、スケジューリングに利用できます。

この例では、ソルバーが実行されると、公開範囲とドラフト範囲の両方を含めた 2 週間の計画枠全体を検索します。ソルバーは、スケジューリング前の入力データに加え、固定されておらず、状態が OpenReschedule または Rejected の予約、エンティティーを検討して、最適解を見つけ出します。ソルバーを日次で実行する場合には、ソルバーを実行する前に新しい日付が追加されることが確認できます。

新しい日付に予約が割り当てられ、固定ウィンドウのドラフト部分で、これまでにスケジュールされていた Amy と Edna が公開部分の枠で予約が取れていることが分かります。これは、Gus および Hugo が再スケジュールを依頼したので可能となりました。Amy と Edna にはドラフトの日付が通知されていないので、混乱を招くことはありません。これで、計画枠の公開部分で予約が取れたので、Amy と Edna には通知が送信され、その予約を承諾するか拒否するかが確認され、この 2 人の予約が固定されます。

11.2. OptaPlanner ワクチン接種予約スケジューラーのダウンロードおよび実行

OptaPlanner ワクチン接種予約スケジューラークイックスタートをダウンロードし、Quarkus の開発モードで起動して、ブラウザーでアプリケーションを表示します。Quarkus 開発モードを使用すると、実行中にアプリケーションを変更し、更新できます。

手順

  1. Red Hat カスタマーポータルの Software Downloads ページに移動し (ログインが必要)、ドロップダウンオプションから製品およびバージョンを選択します。

    • 製品: Red Hat build of OptaPlanner
    • バージョン: 8.33
  2. Red Hat build of OptaPlanner 8.33 クイックスタート をダウンロードします。
  3. rhbop-8.33.0-optaplanner-quickstarts-sources.zip ファイルを展開します。

    展開先の org.optaplanner.optaplanner-quickstarts-8.33.0.Final-redhat-00004/use-cases/vaccination-scheduling ディレクトリーには、サンプルソースコードが含まれています。

  4. org.optaplanner.optaplanner-quickstarts-8.33.0.Final-redhat-00004/use-cases/vaccination-scheduling ディレクトリーに移動します。
  5. 以下のコマンドを入力して、開発モードで OptaPlanner 解析スケジューラーを起動します。

    $ mvn quarkus:dev
  6. OptaPlanner ワクチン接種予約スケジューラーを表示するには、Web ブラウザーに以下の URL を入力します。

    http://localhost:8080/
  7. OptaPlanner ワクチン接種予約スケジューラーを実行するには、Solve をクリックします。
  8. ソースコードに変更を加えてから、F5 キーを押してブラウザーを更新します。加えた変更が有効になったことを確認してください。

11.3. OptaPlanner ワクチン接種予約スケジューラーのパッケージ化および実行

quarkus:dev モードの OptaPlanner ワクチン接種予約スケジューラーで開発作業が完了したら、アプリケーションを従来の jar ファイルとして実行します。

前提条件

  • OptaPlanner ワクチン接種予約スケジューラーのクイックスタートをダウンロードしている。

手順

  1. /use-cases/vaccination-scheduling ディレクトリーに移動します。
  2. OptaPlanner 解析スケジューラーをコンパイルするには、以下のコマンドを入力します。

    $ mvn package
  3. コンパイルした OptaPlanner ワクチン接種予約スケジューラーを実行するには、以下のコマンドを入力します。

    $ java -jar ./target/quarkus-app/quarkus-run.jar
    注記

    ポート 8081 でアプリケーションを実行するには、-Dquarkus.http.port=8081 を前述のコマンドに追加します。

  4. OptaPlanner ワクチン接種予約スケジューラーを起動するには、Web ブラウザーに以下の URL を入力します。

    http://localhost:8080/

11.4. 関連情報

第12章 Red Hat build of Quarkus プラットフォーム上の Red Hat build of OptaPlanner: 従業員スケジューラーのクイックスタートガイド

従業員スケジューラークイックスタートアプリケーションは、従業員を組織内のさまざまな役職のシフトに割り当てます。たとえば、アプリケーションを使用して、病院での看護師のシフト、さまざまな場所での警備勤務シフト、作業者の組み立てラインのシフトを割り当てます。

最適な従業員のスケジューリングでは、多くの変数を考慮に入れる必要があります。たとえば、業務が異なれば、求められるスキルが異なります。また、従業員の中には、特定の時間帯に勤務できない場合や、特定の時間帯での勤務を希望する場合があります。さらに、従業員によっては、1 回に就業できる時間に制限がある契約を交わしている可能性があります。

このスターターアプリケーションの Red Hat build of OptaPlanner ルールは、ハード制約およびソフト制約を使用します。最適化時に、従業員が勤務できない (または病欠の) 場合や、ある 1 つのシフト内の 2 つのスポットで働くことができない場合など、Planner エンジンはハード制約に違反することができません。Planner エンジンは、ソフト制約 (特定のシフトで勤務しないという従業員の希望など) に順守しようとしますが、最適なソリューションには違反が必要だと判断した場合は、違反することができます。

前提条件

  • OpenJDK 11 以降がインストールされている。Red Hat ビルドの Open JDK は Red Hat カスマーポータル (ログインが必要) の ソフトウェアダウンロード ページから入手できます。
  • Apache Maven 3.8 以降がインストールされている。Maven は Apache Maven Project の Web サイトから入手できます。
  • IntelliJ IDEA、VSCode、Eclipse などの IDE が利用できる。

12.1. OptaPlanner 従業員スケジューラーのダウンロードと実行

OptaPlanner 従業員スケジューラークイックスタートアーカイブをダウンロードし、Quarkus 開発モードで起動して、ブラウザーでアプリケーションを表示します。Quarkus 開発モードを使用すると、実行中にアプリケーションを変更し、更新できます。

手順

  1. Red Hat カスタマーポータルの Software Downloads ページに移動し (ログインが必要)、ドロップダウンオプションから製品およびバージョンを選択します。

    • 製品: Red Hat build of OptaPlanner
    • バージョン: 8.33
  2. Red Hat build of OptaPlanner 8.33 クイックスタート をダウンロードします。
  3. rhbop-8.33.0-optaplanner-quickstarts-sources.zip ファイルを展開します。
  4. org.optaplanner.optaplanner-quickstarts-8.33.0.Final-redhat-00004/use-cases/employee-scheduling ディレクトリーに移動します。
  5. 次のコマンドを入力して、OptaPlanner 従業員スケジューラーを開発モードで開始します。

    $ mvn quarkus:dev
  6. OptaPlanner 従業員スケジューラーを表示するには、Web ブラウザーに次の URL を入力します。

    http://localhost:8080/
  7. OptaPlanner 従業員スケジューラーを実行するには、Solve をクリックします。
  8. ソースコードに変更を加えてから、F5 キーを押してブラウザーを更新します。加えた変更が有効になったことを確認してください。

12.2. OptaPlanner 従業員スケジューラーのパッケージ化および実行

quarkus:dev モードで OptaPlanner 従業員スケジューラーの開発作業が完了したら、アプリケーションを従来の jar ファイルとして実行します。

前提条件

  • OptaPlanner 従業員スケジューリングクイックスタートをダウンロードしている。

手順

  1. /use-cases/vaccination-scheduling ディレクトリーに移動します。
  2. OptaPlanner 従業員スケジューラーをコンパイルするには、次のコマンドを入力します。

    $ mvn package
  3. コンパイルされた OptaPlanner 従業員スケジューラーを実行するには、次のコマンドを入力します。

    $ java -jar ./target/quarkus-app/quarkus-run.jar
    注記

    ポート 8081 でアプリケーションを実行するには、-Dquarkus.http.port=8081 を前述のコマンドに追加します。

  4. OptaPlanner 従業員スケジューラーを開始するには、Web ブラウザーに次の URL を入力します。

    http://localhost:8080/

第13章 Spring Boot 上の Red Hat build of OptaPlanner: 時間割のクイックスタートガイド

本書では、OptaPlanner の制約解決人工知能 (AI) を使用して Spring Boot アプリケーションを作成するプロセスを説明します。生徒と教師の時間割を最適化する REST アプリケーションを構築します。

学校の時間割解の図

サービスは、AI を使用して、以下のハードおよびソフトの スケジュール制約 に準拠し、Lesson インスタンスを Timeslot インスタンスと Room インスタンスに自動的に割り当てます。

  • 1 部屋に同時に割り当てることができる授業は、最大 1 コマです。
  • 教師が同時に一度に行うことができる授業は最大 1 回です。
  • 生徒は同時に出席できる授業は最大 1 コマです。
  • 教師は、1 つの部屋での授業を希望します。
  • 教師は、連続した授業を好み、授業間に時間が空くのを嫌います。

数学的に考えると、学校の時間割は NP 困難 の問題であります。つまり、スケーリングが困難です。総当たり攻撃で考えられる組み合わせを単純にすべて反復すると、スーパーコンピューターを使用したとしても、非自明的なデータセットを取得するのに数百年かかります。幸い、OptaPlanner などの AI 制約ソルバーには、妥当な時間内にほぼ最適なソリューションを提供する高度なアルゴリズムがあります。妥当な期間として考慮される内容は、問題の目的によって異なります。

前提条件

  • OpenJDK 11 以降がインストールされている。Red Hat ビルドの Open JDK は Red Hat カスマーポータル (ログインが必要) の ソフトウェアダウンロード ページから入手できます。
  • Apache Maven 3.8 以降がインストールされている。Maven は Apache Maven Project の Web サイトから入手できます。
  • IntelliJ IDEA、VSCode、Eclipse などの IDE が利用できる。

13.1. Spring Boot 時間割のクイックスタートのダウンロードおよびビルド

Spring Boot 製品を備えた Red Hat build of OptaPlanner 向けの授業の時間割プロジェクトの完全な例を表示するには、Red Hat カスタマーポータルからスターターアプリケーションをダウンロードします。

手順

  1. Red Hat カスタマーポータルの Software Downloads ページに移動し (ログインが必要)、ドロップダウンオプションから製品およびバージョンを選択します。

    • 製品: Red Hat build of OptaPlanner
    • バージョン: 8.33
  2. Red Hat build of OptaPlanner 8.33 クイックスタート をダウンロードします。
  3. rhbop-8.33.0-optaplanner-quickstarts-sources.zip ファイルを展開します。

    展開先の org.optaplanner.optaplanner-quickstarts-8.33.0.Final-redhat-00004/use-cases/school-timetabling ディレクトリーには、サンプルソースコードが含まれています。

  4. org.optaplanner.optaplanner-quickstarts-8.33.0.Final-redhat-00004/use-cases/school-timetabling ディレクトリーに移動します。
  5. OptaPlanner 8.33.0 Maven Repositroy の Red Hat ビルド (rhbop-8.33.0-optaplanner-maven-repository.zip) をダウンロードします。
  6. rhbop-8.33.0-optaplanner-maven-repository.zip ファイルを展開します。
  7. rhbop-8.33.0-optaplanner/maven-repository サブディレクトリーの内容を ~/.m2/repository ディレクトリーにコピーします。
  8. org.optaplanner.optaplanner-quickstarts-8.33.0.Final-redhat-00004/technology/java-spring-boot ディレクトリーに移動します。
  9. 以下のコマンドを入力して、Spring Boot の授業の時間割プロジェクトをビルドします。

    mvn clean install -DskipTests
  10. Spring Boot の授業の時間割プロジェクトをビルドするには、以下のコマンドを入力します。

    mvn spring-boot:run -DskipTests
  11. プロジェクトを表示するには、Web ブラウザーで以下の URL を入力します。

    http://localhost:8080/

13.2. ドメインオブジェクトのモデル化

Red Hat build of OptaPlanner の時間割プロジェクトの目標は、レッスンごとに時間枠と部屋に割り当てることです。これには、次の図に示すように、TimeslotLesson、および Room の 3 つのクラスを追加します。

タイムスロットを示す時間割クラス図

Timeslot

Timeslot クラスは、Monday 10:30 - 11:30Tuesday 13:30 - 14:30 など、授業の長さを表します。この例では、時間枠はすべて同じ長さ (期間) で、昼休みまたは他の休憩時間にはこのスロットはありません。

高校のスケジュールは毎週 (同じ内容が) 繰り返されるだけなので、時間枠には日付がありません。また、継続的プランニング は必要ありません。解決時に Timeslot インスタンスが変更しないため、Timeslot は 問題ファクト と呼ばれます。このようなクラスには OptaPlanner 固有のアノテーションは必要ありません。

Room

Room クラスは、Room ARoom B など、授業の場所を表します。以下の例では、どの部屋も定員制限がなく、すべての授業に対応できます。

Room インスタンスは解決時に変化しないため、Room問題ファクト でもあります。

Lesson

授業中 (Lesson クラスで表現)、教師は複数の生徒に Math by A.Turing for 9th gradeChemistry by M.Curie for 10th grade などの教科を指導します。ある教科について、毎週複数回、同じ教師が同じ生徒グループを指導する場合は、Lesson インスタンスが複数使用されますが、それらは id で識別可能です。たとえば、9 年生の場合は、1 週間に 6 回数学の授業があります。

解決中に、OptaPlanner は、Lesson クラスの timeslot フィールドと room フィールドを変更して、各授業を、時間枠 1 つ、部屋 1 つに割り当てます。OptaPlanner はこれらのフィールドを変更するため、Lessonプランニングエンティティー となります。

計画変数と対話する Timeslot クラスと Room クラスを示す Timetable クラス図

前図では、オレンジのフィールド以外のほぼすべてのフィールドに、入力データが含まれています。授業の timeslot フィールドと room フィールドは、入力データに割り当てられておらず (null)、出力データに割り当てられて (null ではない) います。Red Hat build of OptaPlanner は、解決時にこれらのフィールドを変更します。このようなフィールドはプランニング変数と呼ばれます。このフィールドを OptaPlanner に認識させるには、timeslot フィールドと room のフィールドに @PlanningVariable アノテーションが必要です。このフィールドに含まれる Lesson クラスには、@PlanningEntity アノテーションが必要です。

手順

  1. src/main/java/com/example/domain/Timeslot.java クラスを作成します。

    package com.example.domain;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    
    public class Timeslot {
    
        private DayOfWeek dayOfWeek;
        private LocalTime startTime;
        private LocalTime endTime;
    
        private Timeslot() {
        }
    
        public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
            this.dayOfWeek = dayOfWeek;
            this.startTime = startTime;
            this.endTime = endTime;
        }
    
        @Override
        public String toString() {
            return dayOfWeek + " " + startTime.toString();
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public DayOfWeek getDayOfWeek() {
            return dayOfWeek;
        }
    
        public LocalTime getStartTime() {
            return startTime;
        }
    
        public LocalTime getEndTime() {
            return endTime;
        }
    
    }

    後述しているように、toString() メソッドで出力を短くするため、OptaPlanner の DEBUG ログまたは TRACE ログの読み取りが簡単になっています。

  2. src/main/java/com/example/domain/Room.java クラスを作成します。

    package com.example.domain;
    
    public class Room {
    
        private String name;
    
        private Room() {
        }
    
        public Room(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public String getName() {
            return name;
        }
    
    }
  3. src/main/java/com/example/domain/Lesson.java クラスを作成します。

    package com.example.domain;
    
    import org.optaplanner.core.api.domain.entity.PlanningEntity;
    import org.optaplanner.core.api.domain.variable.PlanningVariable;
    
    @PlanningEntity
    public class Lesson {
    
        private Long id;
    
        private String subject;
        private String teacher;
        private String studentGroup;
    
        @PlanningVariable(valueRangeProviderRefs = "timeslotRange")
        private Timeslot timeslot;
    
        @PlanningVariable(valueRangeProviderRefs = "roomRange")
        private Room room;
    
        private Lesson() {
        }
    
        public Lesson(Long id, String subject, String teacher, String studentGroup) {
            this.id = id;
            this.subject = subject;
            this.teacher = teacher;
            this.studentGroup = studentGroup;
        }
    
        @Override
        public String toString() {
            return subject + "(" + id + ")";
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public Long getId() {
            return id;
        }
    
        public String getSubject() {
            return subject;
        }
    
        public String getTeacher() {
            return teacher;
        }
    
        public String getStudentGroup() {
            return studentGroup;
        }
    
        public Timeslot getTimeslot() {
            return timeslot;
        }
    
        public void setTimeslot(Timeslot timeslot) {
            this.timeslot = timeslot;
        }
    
        public Room getRoom() {
            return room;
        }
    
        public void setRoom(Room room) {
            this.room = room;
        }
    
    }

    Lesson クラスには @PlanningEntity アノテーションが含まれており、その中にプランニング変数が 1 つ以上含まれているため、OptaPlanner はこのクラスが解決時に変化することを認識します。

    timeslot フィールドには @PlanningVariable アノテーションがあるため、OptaPlanner は、このフィールドの値が変化することを認識しています。このフィールドに割り当てることのできる Timeslot インスタンスを見つけ出すために、OptaPlanner は valueRangeProviderRefs プロパティーを使用して値の範囲プロバイダーと連携し、List<Timeslot> を提供して選択できるようにします。値の範囲プロバイダーに関する詳細は、「プランニングソリューションでのドメインオブジェクトの収集」 を参照してください。

    room フィールドにも、同じ理由で @PlanningVariable アノテーションが含まれます。

13.3. 制約の定義およびスコアの計算

問題の解決時に スコア で導かれた解の質を表します。スコアが高いほど質が高くなります。Red Hat build of OptaPlanner は、利用可能な時間内で見つかった解の中から最高スコアのものを探し出します。これが 最適 解である可能性があります。

時間割の例のユースケースでは、ハードとソフト制約を使用しているため、HardSoftScore クラスでスコアを表します。

  • ハード制約は、絶対に違反しないでください。たとえば、部屋に同時に割り当てることができる授業は、最大 1 コマです。
  • ソフト制約は、違反しないようにしてください。たとえば、教師は、1 つの部屋での授業を希望します。

ハード制約は、他のハード制約と比べて、重み付けを行います。ソフト制約は、他のソフト制約と比べて、重み付けを行います。ハード制約は、それぞれの重みに関係なく、常にソフト制約よりも高くなります。

EasyScoreCalculator クラスを実装して、スコアを計算できます。

public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> {

    @Override
    public HardSoftScore calculateScore(TimeTable timeTable) {
        List<Lesson> lessonList = timeTable.getLessonList();
        int hardScore = 0;
        for (Lesson a : lessonList) {
            for (Lesson b : lessonList) {
                if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot())
                        && a.getId() < b.getId()) {
                    // A room can accommodate at most one lesson at the same time.
                    if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) {
                        hardScore--;
                    }
                    // A teacher can teach at most one lesson at the same time.
                    if (a.getTeacher().equals(b.getTeacher())) {
                        hardScore--;
                    }
                    // A student can attend at most one lesson at the same time.
                    if (a.getStudentGroup().equals(b.getStudentGroup())) {
                        hardScore--;
                    }
                }
            }
        }
        int softScore = 0;
        // Soft constraints are only implemented in the "complete" implementation
        return HardSoftScore.of(hardScore, softScore);
    }

}

残念ながら、この解は漸増的ではないので、適切にスケーリングされません。授業が別の時間枠や教室に割り当てられるたびに、全授業が再評価され、新しいスコアが計算されます。

src/main/java/com/example/solver/TimeTableConstraintProvider.java クラスを作成して、漸増的スコア計算を実行すると、解がより優れたものになります。このクラスは、Java 8 Streams と SQL を基にした OptaPlanner の ConstraintStream API を使用します。ConstraintProvider は、EasyScoreCalculator と比べ、スケーリングの規模が遥かに大きくなっています (O (n²) ではなく O (n))。

手順

以下の src/main/java/com/example/solver/TimeTableConstraintProvider.java クラスを作成します。

package com.example.solver;

import com.example.domain.Lesson;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaplanner.core.api.score.stream.Joiners;

public class TimeTableConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                // Hard constraints
                roomConflict(constraintFactory),
                teacherConflict(constraintFactory),
                studentGroupConflict(constraintFactory),
                // Soft constraints are only implemented in the "complete" implementation
        };
    }

    private Constraint roomConflict(ConstraintFactory constraintFactory) {
        // A room can accommodate at most one lesson at the same time.

        // Select a lesson ...
        return constraintFactory.forEach(Lesson.class)
                // ... and pair it with another lesson ...
                .join(Lesson.class,
                        // ... in the same timeslot ...
                        Joiners.equal(Lesson::getTimeslot),
                        // ... in the same room ...
                        Joiners.equal(Lesson::getRoom),
                        // ... and the pair is unique (different id, no reverse pairs)
                        Joiners.lessThan(Lesson::getId))
                // then penalize each pair with a hard weight.
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("Room conflict");
    }

    private Constraint teacherConflict(ConstraintFactory constraintFactory) {
        // A teacher can teach at most one lesson at the same time.
        return constraintFactory.forEach(Lesson.class)
                .join(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getTeacher),
                        Joiners.lessThan(Lesson::getId))
                        .penalize(HardSoftScore.ONE_HARD)
                        .asConstraint("Teacher conflict");
    }

    private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
        // A student can attend at most one lesson at the same time.
        return constraintFactory.forEach(Lesson.class)
                .join(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getStudentGroup),
                        Joiners.lessThan(Lesson::getId))
                        .penalize(HardSoftScore.ONE_HARD)
                        .asConstraint("Student group conflict");
    }

}

13.4. プランニングソリューションでのドメインオブジェクトの収集

TimeTable インスタンスは、単一データセットの Timeslot インスタンス、Room インスタンス、および Lesson インスタンスをラップします。さらに、このインスタンスは、特定のプランニング変数の状態を持つ授業がすべて含まれているため、このインスタンスは プランニングソリューション となり、スコアが割り当てられます。

  • 授業がまだ割り当てられていない場合は、スコアが -4init/0hard/0soft のソリューションなど、初期化されていない ソリューションとなります。
  • ハード制約に違反する場合、スコアが -2hard/-3soft のソリューションなど、実行不可 なソリューションとなります。
  • 全ハード制約に準拠している場合は、スコアが 0hard/-7soft など、実行可能 なソリューションとなります。

TimeTable クラスには @PlanningSolution アノテーションが含まれているため、Red Hat build of OptaPlanner はこのクラスに全入出力データが含まれていることを認識します。

具体的には、このクラスは問題の入力です。

  • 全時間枠が含まれる timeslotList フィールド

    • これは、解決時に変更されないため、問題ファクトリーストです。
  • 全部屋が含まれる roomList フィールド

    • これは、解決時に変更されないため、問題ファクトリーストです。
  • 全授業が含まれる lessonList フィールド

    • これは、解決時に変更されるため、プランニングエンティティーです。
    • Lesson:

      • timeslot フィールドおよび room フィールドの値は通常、null で未割り当てです。これらの値は、プランニング変数です。
      • subjectteacherstudentGroup などの他のフィールドは入力されます。これらのフィールドは問題プロパティーです。

ただし、このクラスはソリューションの出力でもあります。

  • Lesson インスタンスごとの lessonList フィールドには、解決後は null ではない timeslot フィールドと room フィールドが含まれます。
  • 出力ソリューションの品質を表す score フィールド (例: 0hard/-5soft)

手順

src/main/java/com/example/domain/TimeTable.java クラスを作成します。

package com.example.domain;

import java.util.List;

import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;

@PlanningSolution
public class TimeTable {

    @ValueRangeProvider(id = "timeslotRange")
    @ProblemFactCollectionProperty
    private List<Timeslot> timeslotList;

    @ValueRangeProvider(id = "roomRange")
    @ProblemFactCollectionProperty
    private List<Room> roomList;

    @PlanningEntityCollectionProperty
    private List<Lesson> lessonList;

    @PlanningScore
    private HardSoftScore score;

    private TimeTable() {
    }

    public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
            List<Lesson> lessonList) {
        this.timeslotList = timeslotList;
        this.roomList = roomList;
        this.lessonList = lessonList;
    }

    // ********************************
    // Getters and setters
    // ********************************

    public List<Timeslot> getTimeslotList() {
        return timeslotList;
    }

    public List<Room> getRoomList() {
        return roomList;
    }

    public List<Lesson> getLessonList() {
        return lessonList;
    }

    public HardSoftScore getScore() {
        return score;
    }

}

値の範囲のプロバイダー

timeslotList フィールドは、値の範囲プロバイダーです。これは Timeslot インスタンスを保持し、OptaPlanner がこのインスタンスを選択して、Lesson インスタンスの timeslot フィールドに割り当てることができます。timeslotList フィールドには @ValueRangeProvider アノテーションがあり、id を、Lesson@PlanningVariablevalueRangeProviderRefs に一致させます。

同じロジックに従い、roomList フィールドにも @ValueRangeProvider アノテーションが含まれています。

問題ファクトとプランニングエンティティーのプロパティー

さらに OptaPlanner は、変更可能な Lesson インスタンス、さらに TimeTableConstraintProvider によるスコア計算に使用する Timeslot インスタンスと Room インスタンスを取得する方法を把握しておく必要があります。

timeslotList フィールドと roomList フィールドには @ProblemFactCollectionProperty アノテーションが含まれているため、TimeTableConstraintProvider はこれらのインスタンスから選択できます。

lessonList には @PlanningEntityCollectionProperty アノテーションが含まれているため、OptaPlanner は解決時に変更でき、TimeTableConstraintProvider はこの中から選択できます。

13.5. Timetable サービスの作成

これですべて組み合わせ、REST サービスを作成する準備ができました。しかし、REST スレッドで計画問題を解決すると、HTTP タイムアウトの問題が発生します。そのため、Spring Boot スターターでは SolverManager を注入することで、個別のスレッドプールでソルバーを実行して複数のデータセットを並行して解決できます。

手順

src/main/java/com/example/solver/TimeTableController.java クラスを作成します。

package com.example.solver;

import java.util.UUID;
import java.util.concurrent.ExecutionException;

import com.example.domain.TimeTable;
import org.optaplanner.core.api.solver.SolverJob;
import org.optaplanner.core.api.solver.SolverManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/timeTable")
public class TimeTableController {

    @Autowired
    private SolverManager<TimeTable, UUID> solverManager;

    @PostMapping("/solve")
    public TimeTable solve(@RequestBody TimeTable problem) {
        UUID problemId = UUID.randomUUID();
        // Submit the problem to start solving
        SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
        TimeTable solution;
        try {
            // Wait until the solving ends
            solution = solverJob.getFinalBestSolution();
        } catch (InterruptedException | ExecutionException e) {
            throw new IllegalStateException("Solving failed.", e);
        }
        return solution;
    }

}

この例では、初期実装はソルバーが完了するのを待つので、HTTP タイムアウトがまだ発生します。complete 実装を使用することで、より適切に HTTP タイムアウトを回避できます。

13.6. ソルバー終了時間の設定

プランニングアプリケーションに終了設定または終了イベントがない場合、理論的には永続的に実行されることになり、実際には HTTP タイムアウトエラーが発生します。これが発生しないようにするには、optaplanner.solver.termination.spent-limit パラメーターを使用して、アプリケーションが終了してからの時間を指定します。多くのアプリケーションでは、この時間を最低でも 5 分 (5m) に設定します。ただし、時間割の例では、解決時間を 5 分に制限すると、期間が十分に短いため HTTP タイムアウトを回避できます。

手順

以下の内容を含む src/main/resources/application.properties ファイルを作成します。

quarkus.optaplanner.solver.termination.spent-limit=5s

13.7. アプリケーションを実行可能にする手順

Red Hat build of OptaPlanner Spring Boot の時間割プロジェクトを完了すると、標準 Java main() メソッドで駆動する 1 つの実行可能 JAR ファイルにすべてをパッケージ化します。

前提条件

  • これで OptaPlanner Spring Boot の時間割プロジェクトが完成しました。

手順

  1. 以下の内容を含む TimeTableSpringBootApp.java クラスを作成します。

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class TimeTableSpringBootApp {
    
        public static void main(String[] args) {
            SpringApplication.run(TimeTableSpringBootApp.class, args);
        }
    
    }
  2. Spring Initializr で作成された src/main/java/com/example/DemoApplication.java クラスは TimeTableSpringBootApp.java クラスに置き換えます。
  3. 通常の Java アプリケーションのメインクラスとして TimeTableSpringBootApp.java クラスを実行します。

13.7.1. 時間割アプリケーションの試行

Red Hat build of OptaPlanner Spring Boot 時間割アプリケーションの起動後に、任意の REST クライアントで REST サービスをテストできます。この例では Linux curl コマンドを使用して POST 要求を送信します。

前提条件

  • OptaPlanner Spring Boot アプリケーションが実行中である。

手順

以下のコマンドを入力します。

$ curl -i -X POST http://localhost:8080/timeTable/solve -H "Content-Type:application/json" -d '{"timeslotList":[{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"}],"roomList":[{"name":"Room A"},{"name":"Room B"}],"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade"},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade"},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade"},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade"}]}'

約 5 秒後 (application.properties で定義された終了時間) に、サービスにより、以下の例のような出力が返されます。

HTTP/1.1 200
Content-Type: application/json
...

{"timeslotList":...,"roomList":...,"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room A"}},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room A"}},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room B"}},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room B"}}],"score":"0hard/0soft"}

アプリケーションにより、4 つの授業がすべて 2 つの時間枠、そして 2 つある部屋のうちの 1 つに割り当てられている点に注目してください。また、すべてのハード制約に準拠することに注意してください。たとえば、M. Curie の 2 つの授業は異なる時間スロットにあります。

サーバー側で info ログに、この 5 秒間で OptaPlanner が行った内容が記録されます。

... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4).
... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398).
... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE).

13.7.2. アプリケーションのテスト

適切なアプリケーションにはテストが含まれます。以下の例では、Red Hat build of OptaPlanner Spring Boot 時間割アプリケーションをテストします。このアプリケーションは、JUnit テストを使用してテストのデータセットを生成し、TimeTableController に送信して解決します。

手順

以下の内容を含む src/test/java/com/example/solver/TimeTableControllerTest.java クラスを作成します。

package com.example.solver;

import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

import com.example.domain.Lesson;
import com.example.domain.Room;
import com.example.domain.TimeTable;
import com.example.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(properties = {
        "optaplanner.solver.termination.spent-limit=1h", // Effectively disable this termination in favor of the best-score-limit
        "optaplanner.solver.termination.best-score-limit=0hard/*soft"})
public class TimeTableControllerTest {

    @Autowired
    private TimeTableController timeTableController;

    @Test
    @Timeout(600_000)
    public void solve() {
        TimeTable problem = generateProblem();
        TimeTable solution = timeTableController.solve(problem);
        assertFalse(solution.getLessonList().isEmpty());
        for (Lesson lesson : solution.getLessonList()) {
            assertNotNull(lesson.getTimeslot());
            assertNotNull(lesson.getRoom());
        }
        assertTrue(solution.getScore().isFeasible());
    }

    private TimeTable generateProblem() {
        List<Timeslot> timeslotList = new ArrayList<>();
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));

        List<Room> roomList = new ArrayList<>();
        roomList.add(new Room("Room A"));
        roomList.add(new Room("Room B"));
        roomList.add(new Room("Room C"));

        List<Lesson> lessonList = new ArrayList<>();
        lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade"));
        lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade"));
        lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade"));
        lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade"));

        lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade"));
        lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade"));
        lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade"));
        lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade"));
        return new TimeTable(timeslotList, roomList, lessonList);
    }

}

このテストは、解決後にすべての授業がタイムスロットと部屋に割り当てられていることを確認します。また、実行可能解 (ハード制約の違反なし) も確認します。

通常、ソルバーは 200 ミリ秒未満で実行可能解を検索します。@SpringBootTest アノテーションの properties が、実行可能なソリューション (0hard/*soft) が見つかると同時に、ソルバーの終了を上書きします。こうすることで、ユニットテストが任意のハードウェアで実行される可能性があるため、ソルバーの時間をハードコード化するのを回避します。このアプローチを使用することで、動きが遅いシステムであっても、実行可能なソリューションを検索するのに十分な時間だけテストが実行されます。ただし、高速マシンでも、厳密に必要とされる時間よりもミリ秒単位で長く実行されることはありません。

13.7.3. ロギング

Red Hat build of OptaPlanner Spring Boot の時間割アプリケーションアプリケーションを完了後にロギング情報を使用すると、ConstraintProvider で制約が微調整しやすくなります。info ログファイルでスコア計算の速度を確認して、制約に加えた変更の影響を評価します。デバッグモードでアプリケーションを実行して、アプリケーションが行う手順をすべて表示するか、追跡ログを使用して全手順および動きをロギングします。

手順

  1. 時間割アプリケーションを一定の時間 (例: 5 分) 実行します。
  2. 以下の例のように、log ファイルのスコア計算の速度を確認します。

    ... Solving ended: ..., score calculation speed (29455/sec), ...
  3. 制約を変更して、同じ時間、プランニングアプリケーションを実行し、log ファイルに記録されているスコア計算速度を確認します。
  4. アプリケーションをデバッグモードで実行して、全手順をログに記録します。

    • コマンドラインからデバッグモードを実行するには、-D システムプロパティーを使用します。
    • application.properties ファイルのロギングを変更するには、以下の行をこのファイルに追加します。

      logging.level.org.optaplanner=debug

      以下の例では、デバッグモードでの log ファイルの出力を表示します。

      ... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
      ...     CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]).
      ...     CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]).
      ...
  5. trace ロギングを使用して、全手順、および手順ごとの全動きを表示します。

13.8. データベースと UI 統合の追加

Spring Boot を使用して Red Hat build of OptaPlanner アプリケーションの例を作成したら、データベースと UI 統合を追加します。

前提条件

  • OptaPlanner Spring Boot の時間割サンプルが作成されている。

手順

  1. TimeslotRoom、および Lesson の Java Persistence API (JPA) リポジトリーを作成します。JPA リポジトリーの作成に関する情報は、Spring の Web サイトの Accessing Data with JPA を参照してください。
  2. REST で JPA リポジトリーを公開します。リポジトリーの公開に関する情報は、Spring の Web サイトの Accessing JPA Data with REST を参照してください。
  3. TimeTableRepository Facade をビルドして、1 回のトランザクションで TimeTable を読み取り、書き込みます。
  4. 以下の例のように TimeTableController を調整します。

    package com.example.solver;
    
    import com.example.domain.TimeTable;
    import com.example.persistence.TimeTableRepository;
    import org.optaplanner.core.api.score.ScoreManager;
    import org.optaplanner.core.api.solver.SolverManager;
    import org.optaplanner.core.api.solver.SolverStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/timeTable")
    public class TimeTableController {
    
        @Autowired
        private TimeTableRepository timeTableRepository;
        @Autowired
        private SolverManager<TimeTable, Long> solverManager;
        @Autowired
        private ScoreManager<TimeTable> scoreManager;
    
        // To try, GET http://localhost:8080/timeTable
        @GetMapping()
        public TimeTable getTimeTable() {
            // Get the solver status before loading the solution
            // to avoid the race condition that the solver terminates between them
            SolverStatus solverStatus = getSolverStatus();
            TimeTable solution = timeTableRepository.findById(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
            scoreManager.updateScore(solution); // Sets the score
            solution.setSolverStatus(solverStatus);
            return solution;
        }
    
        @PostMapping("/solve")
        public void solve() {
            solverManager.solveAndListen(TimeTableRepository.SINGLETON_TIME_TABLE_ID,
                    timeTableRepository::findById,
                    timeTableRepository::save);
        }
    
        public SolverStatus getSolverStatus() {
            return solverManager.getSolverStatus(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
        }
    
        @PostMapping("/stopSolving")
        public void stopSolving() {
            solverManager.terminateEarly(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
        }
    
    }

    簡素化するために、このコードは TimeTable インスタンスを 1 つだけ処理しますが、マルチテナントを有効にして、異なる高校の TimeTable インスタンスを複数、平行して処理することも簡単にできます。

    getTimeTable() メソッドは、データベースから最新の時間割を返します。このメソッドは、ScoreManager (自動注入) を使用して、UI でスコアを表示できるように、対象の時間割のスコアを計算します。

    solve() メソッドは、ジョブを開始して、現在の時間割を解決し、時間枠と部屋の割り当てをデータベースに保存します。このメソッドは、SolverManager.solveAndListen() メソッドを使用して、中間の最適解をリッスンし、それに合わせてデータベースを更新します。こうすることで、バックエンドで解決しながら、UI で進捗を表示できます。

  5. solve() メソッドが即座に返されるようになったので、以下の例のように TimeTableControllerTest を調整します。

    package com.example.solver;
    
    import com.example.domain.Lesson;
    import com.example.domain.TimeTable;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    import org.optaplanner.core.api.solver.SolverStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @SpringBootTest(properties = {
            "optaplanner.solver.termination.spent-limit=1h", // Effectively disable this termination in favor of the best-score-limit
            "optaplanner.solver.termination.best-score-limit=0hard/*soft"})
    public class TimeTableControllerTest {
    
        @Autowired
        private TimeTableController timeTableController;
    
        @Test
        @Timeout(600_000)
        public void solveDemoDataUntilFeasible() throws InterruptedException {
            timeTableController.solve();
            TimeTable timeTable = timeTableController.getTimeTable();
            while (timeTable.getSolverStatus() != SolverStatus.NOT_SOLVING) {
                // Quick polling (not a Test Thread Sleep anti-pattern)
                // Test is still fast on fast systems and doesn't randomly fail on slow systems.
                Thread.sleep(20L);
                timeTable = timeTableController.getTimeTable();
            }
            assertFalse(timeTable.getLessonList().isEmpty());
            for (Lesson lesson : timeTable.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(timeTable.getScore().isFeasible());
        }
    
    }
  6. ソルバーの解決が完了するまで、最新のソリューションをポーリングします。
  7. 時間割を視覚化するには、これらの REST メソッドの上に適切な Web UI を構築します。

13.9. Micrometer と Prometheus を使用して学校の時間割を監視する OptaPlanner Spring Boot アプリケーション

OptaPlanner は、Java アプリケーション用のメトリック計測ライブラリーである Micrometer を介してメトリックを公開します。Prometheus で Micrometer を使用して、学校の時間割アプリケーションで OptaPlanner ソルバーを監視できます。

前提条件

  • Spring Boot OptaPlanner 学校の時間割アプリケーションを作成しました。
  • Prometheus がインストールされている。Prometheus のインストールについては、Prometheus の Web サイトを参照してください。

手順

  1. technology/java-spring-boot ディレクトリーに移動します。
  2. 学校の時間割 pom.xml ファイルに Micrometer Prometheus 依存関係を追加します。

    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
  3. 次のプロパティーを application.properties ファイルに追加します。

    management.endpoints.web.exposure.include=metrics,prometheus
  4. 学校の時間割アプリケーションを開始します。

    mvn spring-boot:run
  5. Web ブラウザーで http://localhost:8080/actuator/prometheus を開きます。

第14章 OptaPlanner と Java の Red Hat ビルド: 学校の時間割のクイックスタートガイド

本書では、OptaPlanner の制約解決人工知能 (AI) を使用してシンプル Java アプリケーションを作成するプロセスを説明します。生徒と教師のために学校の時間割を最適化するコマンドラインアプリケーションを作成します。

...
INFO  Solving ended: time spent (5000), best score (0hard/9soft), ...
INFO
INFO  |            | Room A     | Room B     | Room C     |
INFO  |------------|------------|------------|------------|
INFO  | MON 08:30  | English    | Math       |            |
INFO  |            | I. Jones   | A. Turing  |            |
INFO  |            | 9th grade  | 10th grade |            |
INFO  |------------|------------|------------|------------|
INFO  | MON 09:30  | History    | Physics    |            |
INFO  |            | I. Jones   | M. Curie   |            |
INFO  |            | 9th grade  | 10th grade |            |
INFO  |------------|------------|------------|------------|
INFO  | MON 10:30  | History    | Physics    |            |
INFO  |            | I. Jones   | M. Curie   |            |
INFO  |            | 10th grade | 9th grade  |            |
INFO  |------------|------------|------------|------------|
...
INFO  |------------|------------|------------|------------|

アプリケーションは、AI を使用してハードスケジュールとソフトスケジュールの 制約 を順守することにより、Lesson インスタンスを タイムスロット インスタンスと Room インスタンスに自動的に割り当てます。次に例を示します。

  • 1 部屋に同時に割り当てることができる授業は、最大 1 コマです。
  • 教師が同時に一度に行うことができる授業は最大 1 回です。
  • 生徒は同時に出席できる授業は最大 1 コマです。
  • 教師は、すべてのレッスンを同じ部屋で教えることを好みます。
  • 教師は、連続した授業を好み、授業間に時間が空くのを嫌います。
  • 生徒は、同じ科目の連続したレッスンを嫌います。

数学的に考えると、学校の時間割は NP 困難 の問題であります。これは、スケーリングが困難であることを意味します。すべての可能な組み合わせを単純に総当たりで反復すると、スーパーコンピューター上でさえ、自明ではないデータセットに対して数百万年かかります。幸い、OptaPlanner などの AI 制約ソルバーには、妥当な時間内にほぼ最適なソリューションを提供する高度なアルゴリズムがあります。

前提条件

  • OpenJDK (JDK) 11 がインストールされている。Red Hat ビルドの Open JDK は Red Hat カスマーポータル (ログインが必要) の ソフトウェアダウンロード ページから入手できます。
  • Apache Maven 3.6 以降がインストールされている。Maven は Apache Maven Project の Web サイトから入手できます。
  • IntelliJ IDEA、VSCode、Eclipse などの IDE

14.1. Maven または Gradle ビルドファイルの作成および依存関係の追加

OptaPlanner 学校の時間割アプリケーションには、Maven または Gradle を使用できます。ビルドファイルを作成したら、次の依存関係を追加します。

  • 学校の時間割問題を解決するための optaplanner-core (コンパイルスコープ)
  • optaplanner-test (テストスコープ) から JUnit への学校の時間割の制約のテスト
  • OptaPlanner が実行するステップを表示するための logback-classic (ランタイムスコープ) などの実装

手順

  1. Maven または Gradle ビルドファイルを作成します。
  2. optaplanner-coreoptaplanner-test、および logback-classic の依存関係をビルドファイルに追加します。

    • Maven の場合、次の依存関係を pom.xml ファイルに追加します。

        <dependency>
          <groupId>org.optaplanner</groupId>
          <artifactId>optaplanner-core</artifactId>
        </dependency>
      
        <dependency>
          <groupId>org.optaplanner</groupId>
          <artifactId>optaplanner-test</artifactId>
          <scope>test</scope>
        </dependency>
      
        <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>1.2.3</version>
        </dependency>

      次の例は、完全な pom.xml ファイルを示しています。

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
      
        <groupId>org.acme</groupId>
        <artifactId>optaplanner-hello-world-school-timetabling-quickstart</artifactId>
        <version>1.0-SNAPSHOT</version>
      
        <properties>
          <maven.compiler.release>11</maven.compiler.release>
          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      
          <version.org.optaplanner>8.33.0.Final-redhat-00004</version.org.optaplanner>
          <version.org.logback>1.2.3</version.org.logback>
      
          <version.compiler.plugin>3.8.1</version.compiler.plugin>
          <version.surefire.plugin>3.0.0-M5</version.surefire.plugin>
          <version.exec.plugin>3.0.0</version.exec.plugin>
        </properties>
      
        <dependencyManagement>
          <dependencies>
            <dependency>
              <groupId>org.optaplanner</groupId>
              <artifactId>optaplanner-bom</artifactId>
              <version>${version.org.optaplanner}</version>
              <type>pom</type>
              <scope>import</scope>
            </dependency>
            <dependency>
              <groupId>ch.qos.logback</groupId>
              <artifactId>logback-classic</artifactId>
              <version>${version.org.logback}</version>
            </dependency>
          </dependencies>
        </dependencyManagement>
      
        <dependencies>
          <dependency>
            <groupId>org.optaplanner</groupId>
            <artifactId>optaplanner-core</artifactId>
          </dependency>
          <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <scope>runtime</scope>
          </dependency>
      
          <!-- Testing -->
          <dependency>
            <groupId>org.optaplanner</groupId>
            <artifactId>optaplanner-test</artifactId>
            <scope>test</scope>
          </dependency>
        </dependencies>
      
        <build>
          <plugins>
            <plugin>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>${version.compiler.plugin}</version>
            </plugin>
            <plugin>
              <artifactId>maven-surefire-plugin</artifactId>
              <version>${version.surefire.plugin}</version>
            </plugin>
            <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>exec-maven-plugin</artifactId>
              <version>${version.exec.plugin}</version>
              <configuration>
                <mainClass>org.acme.schooltimetabling.TimeTableApp</mainClass>
              </configuration>
            </plugin>
          </plugins>
        </build>
      
        <repositories>
          <repository>
            <id>jboss-public-repository-group</id>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
            <releases>
              <!-- Get releases only from Maven Central which is faster. -->
              <enabled>false</enabled>
            </releases>
            <snapshots>
              <enabled>true</enabled>
            </snapshots>
          </repository>
        </repositories>
      </project>
    • Gradle の場合、次の依存関係を gradle.build ファイルに追加します。

      dependencies {
          implementation platform("org.optaplanner:optaplanner-bom:${optaplannerVersion}")
          implementation "org.optaplanner:optaplanner-core"
          testImplementation "org.optaplanner:optaplanner-test"
      
          runtimeOnly "ch.qos.logback:logback-classic:${logbackVersion}"
      }

      次の例は、完成した gradle.build ファイルを示しています。

      plugins {
          id "java"
          id "application"
      }
      
      def optaplannerVersion = "{optaplanner-version}"
      def logbackVersion = "1.2.9"
      
      group = "org.acme"
      version = "1.0-SNAPSHOT"
      
      repositories {
          mavenCentral()
      }
      
      dependencies {
          implementation platform("org.optaplanner:optaplanner-bom:${optaplannerVersion}")
          implementation "org.optaplanner:optaplanner-core"
          testImplementation "org.optaplanner:optaplanner-test"
      
          runtimeOnly "ch.qos.logback:logback-classic:${logbackVersion}"
      }
      
      java {
          sourceCompatibility = JavaVersion.VERSION_11
          targetCompatibility = JavaVersion.VERSION_11
      }
      
      compileJava {
          options.encoding = "UTF-8"
          options.compilerArgs << "-parameters"
      }
      
      compileTestJava {
          options.encoding = "UTF-8"
      }
      
      application {
          mainClass = "org.acme.schooltimetabling.TimeTableApp"
      }
      
      test {
          // Log the test execution results.
          testLogging {
              events "passed", "skipped", "failed"
          }
      }

14.2. ドメインオブジェクトのモデル化

Red Hat build of OptaPlanner の時間割プロジェクトの目標は、レッスンごとに時間枠と部屋に割り当てることです。これには、次の図に示すように、TimeslotLesson、および Room の 3 つのクラスを追加します。

タイムスロットを示す時間割クラス図

Timeslot

Timeslot クラスは、Monday 10:30 - 11:30Tuesday 13:30 - 14:30 など、授業の長さを表します。この例では、時間枠はすべて同じ長さ (期間) で、昼休みまたは他の休憩時間にはこのスロットはありません。

高校のスケジュールは毎週 (同じ内容が) 繰り返されるだけなので、時間枠には日付がありません。また、継続的プランニング は必要ありません。解決時に Timeslot インスタンスが変更しないため、Timeslot は 問題ファクト と呼ばれます。このようなクラスには OptaPlanner 固有のアノテーションは必要ありません。

Room

Room クラスは、Room ARoom B など、授業の場所を表します。以下の例では、どの部屋も定員制限がなく、すべての授業に対応できます。

Room インスタンスは解決時に変化しないため、Room問題ファクト でもあります。

Lesson

授業中 (Lesson クラスで表現)、教師は複数の生徒に Math by A.Turing for 9th gradeChemistry by M.Curie for 10th grade などの教科を指導します。ある教科について、毎週複数回、同じ教師が同じ生徒グループを指導する場合は、Lesson インスタンスが複数使用されますが、それらは id で識別可能です。たとえば、9 年生の場合は、1 週間に 6 回数学の授業があります。

解決中に、OptaPlanner は、Lesson クラスの timeslot フィールドと room フィールドを変更して、各授業を、時間枠 1 つ、部屋 1 つに割り当てます。OptaPlanner はこれらのフィールドを変更するため、Lessonプランニングエンティティー となります。

計画変数と対話する Timeslot クラスと Room クラスを示す Timetable クラス図

前図では、オレンジのフィールド以外のほぼすべてのフィールドに、入力データが含まれています。授業の timeslot フィールドと room フィールドは、入力データに割り当てられておらず (null)、出力データに割り当てられて (null ではない) います。Red Hat build of OptaPlanner は、解決時にこれらのフィールドを変更します。このようなフィールドはプランニング変数と呼ばれます。このフィールドを OptaPlanner に認識させるには、timeslot フィールドと room のフィールドに @PlanningVariable アノテーションが必要です。このフィールドに含まれる Lesson クラスには、@PlanningEntity アノテーションが必要です。

手順

  1. src/main/java/com/example/domain/Timeslot.java クラスを作成します。

    package com.example.domain;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    
    public class Timeslot {
    
        private DayOfWeek dayOfWeek;
        private LocalTime startTime;
        private LocalTime endTime;
    
        private Timeslot() {
        }
    
        public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
            this.dayOfWeek = dayOfWeek;
            this.startTime = startTime;
            this.endTime = endTime;
        }
    
        @Override
        public String toString() {
            return dayOfWeek + " " + startTime.toString();
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public DayOfWeek getDayOfWeek() {
            return dayOfWeek;
        }
    
        public LocalTime getStartTime() {
            return startTime;
        }
    
        public LocalTime getEndTime() {
            return endTime;
        }
    
    }

    後述しているように、toString() メソッドで出力を短くするため、OptaPlanner の DEBUG ログまたは TRACE ログの読み取りが簡単になっています。

  2. src/main/java/com/example/domain/Room.java クラスを作成します。

    package com.example.domain;
    
    public class Room {
    
        private String name;
    
        private Room() {
        }
    
        public Room(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public String getName() {
            return name;
        }
    
    }
  3. src/main/java/com/example/domain/Lesson.java クラスを作成します。

    package com.example.domain;
    
    import org.optaplanner.core.api.domain.entity.PlanningEntity;
    import org.optaplanner.core.api.domain.variable.PlanningVariable;
    
    @PlanningEntity
    public class Lesson {
    
        private Long id;
    
        private String subject;
        private String teacher;
        private String studentGroup;
    
        @PlanningVariable(valueRangeProviderRefs = "timeslotRange")
        private Timeslot timeslot;
    
        @PlanningVariable(valueRangeProviderRefs = "roomRange")
        private Room room;
    
        private Lesson() {
        }
    
        public Lesson(Long id, String subject, String teacher, String studentGroup) {
            this.id = id;
            this.subject = subject;
            this.teacher = teacher;
            this.studentGroup = studentGroup;
        }
    
        @Override
        public String toString() {
            return subject + "(" + id + ")";
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public Long getId() {
            return id;
        }
    
        public String getSubject() {
            return subject;
        }
    
        public String getTeacher() {
            return teacher;
        }
    
        public String getStudentGroup() {
            return studentGroup;
        }
    
        public Timeslot getTimeslot() {
            return timeslot;
        }
    
        public void setTimeslot(Timeslot timeslot) {
            this.timeslot = timeslot;
        }
    
        public Room getRoom() {
            return room;
        }
    
        public void setRoom(Room room) {
            this.room = room;
        }
    
    }

    Lesson クラスには @PlanningEntity アノテーションが含まれており、その中にプランニング変数が 1 つ以上含まれているため、OptaPlanner はこのクラスが解決時に変化することを認識します。

    timeslot フィールドには @PlanningVariable アノテーションがあるため、OptaPlanner は、このフィールドの値が変化することを認識しています。このフィールドに割り当てることのできる Timeslot インスタンスを見つけ出すために、OptaPlanner は valueRangeProviderRefs プロパティーを使用して値の範囲プロバイダーと連携し、List<Timeslot> を提供して選択できるようにします。値の範囲プロバイダーに関する詳細は、「プランニングソリューションでのドメインオブジェクトの収集」 を参照してください。

    room フィールドにも、同じ理由で @PlanningVariable アノテーションが含まれます。

14.3. 制約の定義およびスコアの計算

問題の解決時に スコア で導かれた解の質を表します。スコアが高いほど質が高くなります。Red Hat build of OptaPlanner は、利用可能な時間内で見つかった解の中から最高スコアのものを探し出します。これが 最適 解である可能性があります。

時間割の例のユースケースでは、ハードとソフト制約を使用しているため、HardSoftScore クラスでスコアを表します。

  • ハード制約は、絶対に違反しないでください。たとえば、部屋に同時に割り当てることができる授業は、最大 1 コマです。
  • ソフト制約は、違反しないようにしてください。たとえば、教師は、1 つの部屋での授業を希望します。

ハード制約は、他のハード制約と比べて、重み付けを行います。ソフト制約は、他のソフト制約と比べて、重み付けを行います。ハード制約は、それぞれの重みに関係なく、常にソフト制約よりも高くなります。

EasyScoreCalculator クラスを実装して、スコアを計算できます。

public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> {

    @Override
    public HardSoftScore calculateScore(TimeTable timeTable) {
        List<Lesson> lessonList = timeTable.getLessonList();
        int hardScore = 0;
        for (Lesson a : lessonList) {
            for (Lesson b : lessonList) {
                if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot())
                        && a.getId() < b.getId()) {
                    // A room can accommodate at most one lesson at the same time.
                    if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) {
                        hardScore--;
                    }
                    // A teacher can teach at most one lesson at the same time.
                    if (a.getTeacher().equals(b.getTeacher())) {
                        hardScore--;
                    }
                    // A student can attend at most one lesson at the same time.
                    if (a.getStudentGroup().equals(b.getStudentGroup())) {
                        hardScore--;
                    }
                }
            }
        }
        int softScore = 0;
        // Soft constraints are only implemented in the "complete" implementation
        return HardSoftScore.of(hardScore, softScore);
    }

}

残念ながら、この解は漸増的ではないので、適切にスケーリングされません。授業が別の時間枠や教室に割り当てられるたびに、全授業が再評価され、新しいスコアが計算されます。

src/main/java/com/example/solver/TimeTableConstraintProvider.java クラスを作成して、漸増的スコア計算を実行すると、解がより優れたものになります。このクラスは、Java 8 Streams と SQL を基にした OptaPlanner の ConstraintStream API を使用します。ConstraintProvider は、EasyScoreCalculator と比べ、スケーリングの規模が遥かに大きくなっています (O (n²) ではなく O (n))。

手順

以下の src/main/java/com/example/solver/TimeTableConstraintProvider.java クラスを作成します。

package com.example.solver;

import com.example.domain.Lesson;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaplanner.core.api.score.stream.Joiners;

public class TimeTableConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                // Hard constraints
                roomConflict(constraintFactory),
                teacherConflict(constraintFactory),
                studentGroupConflict(constraintFactory),
                // Soft constraints are only implemented in the "complete" implementation
        };
    }

    private Constraint roomConflict(ConstraintFactory constraintFactory) {
        // A room can accommodate at most one lesson at the same time.

        // Select a lesson ...
        return constraintFactory.forEach(Lesson.class)
                // ... and pair it with another lesson ...
                .join(Lesson.class,
                        // ... in the same timeslot ...
                        Joiners.equal(Lesson::getTimeslot),
                        // ... in the same room ...
                        Joiners.equal(Lesson::getRoom),
                        // ... and the pair is unique (different id, no reverse pairs)
                        Joiners.lessThan(Lesson::getId))
                // then penalize each pair with a hard weight.
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("Room conflict");
    }

    private Constraint teacherConflict(ConstraintFactory constraintFactory) {
        // A teacher can teach at most one lesson at the same time.
        return constraintFactory.forEach(Lesson.class)
                .join(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getTeacher),
                        Joiners.lessThan(Lesson::getId))
                        .penalize(HardSoftScore.ONE_HARD)
                        .asConstraint("Teacher conflict");
    }

    private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
        // A student can attend at most one lesson at the same time.
        return constraintFactory.forEach(Lesson.class)
                .join(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getStudentGroup),
                        Joiners.lessThan(Lesson::getId))
                        .penalize(HardSoftScore.ONE_HARD)
                        .asConstraint("Student group conflict");
    }

}

14.4. プランニングソリューションでのドメインオブジェクトの収集

TimeTable インスタンスは、単一データセットの Timeslot インスタンス、Room インスタンス、および Lesson インスタンスをラップします。さらに、このインスタンスは、特定のプランニング変数の状態を持つ授業がすべて含まれているため、このインスタンスは プランニングソリューション となり、スコアが割り当てられます。

  • 授業がまだ割り当てられていない場合は、スコアが -4init/0hard/0soft のソリューションなど、初期化されていない ソリューションとなります。
  • ハード制約に違反する場合、スコアが -2hard/-3soft のソリューションなど、実行不可 なソリューションとなります。
  • 全ハード制約に準拠している場合は、スコアが 0hard/-7soft など、実行可能 なソリューションとなります。

TimeTable クラスには @PlanningSolution アノテーションが含まれているため、Red Hat build of OptaPlanner はこのクラスに全入出力データが含まれていることを認識します。

具体的には、このクラスは問題の入力です。

  • 全時間枠が含まれる timeslotList フィールド

    • これは、解決時に変更されないため、問題ファクトリーストです。
  • 全部屋が含まれる roomList フィールド

    • これは、解決時に変更されないため、問題ファクトリーストです。
  • 全授業が含まれる lessonList フィールド

    • これは、解決時に変更されるため、プランニングエンティティーです。
    • Lesson:

      • timeslot フィールドおよび room フィールドの値は通常、null で未割り当てです。これらの値は、プランニング変数です。
      • subjectteacherstudentGroup などの他のフィールドは入力されます。これらのフィールドは問題プロパティーです。

ただし、このクラスはソリューションの出力でもあります。

  • Lesson インスタンスごとの lessonList フィールドには、解決後は null ではない timeslot フィールドと room フィールドが含まれます。
  • 出力ソリューションの品質を表す score フィールド (例: 0hard/-5soft)

手順

src/main/java/com/example/domain/TimeTable.java クラスを作成します。

package com.example.domain;

import java.util.List;

import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;

@PlanningSolution
public class TimeTable {

    @ValueRangeProvider(id = "timeslotRange")
    @ProblemFactCollectionProperty
    private List<Timeslot> timeslotList;

    @ValueRangeProvider(id = "roomRange")
    @ProblemFactCollectionProperty
    private List<Room> roomList;

    @PlanningEntityCollectionProperty
    private List<Lesson> lessonList;

    @PlanningScore
    private HardSoftScore score;

    private TimeTable() {
    }

    public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
            List<Lesson> lessonList) {
        this.timeslotList = timeslotList;
        this.roomList = roomList;
        this.lessonList = lessonList;
    }

    // ********************************
    // Getters and setters
    // ********************************

    public List<Timeslot> getTimeslotList() {
        return timeslotList;
    }

    public List<Room> getRoomList() {
        return roomList;
    }

    public List<Lesson> getLessonList() {
        return lessonList;
    }

    public HardSoftScore getScore() {
        return score;
    }

}

値の範囲のプロバイダー

timeslotList フィールドは、値の範囲プロバイダーです。これは Timeslot インスタンスを保持し、OptaPlanner がこのインスタンスを選択して、Lesson インスタンスの timeslot フィールドに割り当てることができます。timeslotList フィールドには @ValueRangeProvider アノテーションがあり、id を、Lesson@PlanningVariablevalueRangeProviderRefs に一致させます。

同じロジックに従い、roomList フィールドにも @ValueRangeProvider アノテーションが含まれています。

問題ファクトとプランニングエンティティーのプロパティー

さらに OptaPlanner は、変更可能な Lesson インスタンス、さらに TimeTableConstraintProvider によるスコア計算に使用する Timeslot インスタンスと Room インスタンスを取得する方法を把握しておく必要があります。

timeslotList フィールドと roomList フィールドには @ProblemFactCollectionProperty アノテーションが含まれているため、TimeTableConstraintProvider はこれらのインスタンスから選択できます。

lessonList には @PlanningEntityCollectionProperty アノテーションが含まれているため、OptaPlanner は解決時に変更でき、TimeTableConstraintProvider はこの中から選択できます。

14.5. TimeTableApp.java クラス

学校の時間割アプリケーションのすべてのコンポーネントを作成したら、それらをすべて TimeTableApp.java クラスにまとめます。

main() メソッドは以下のタスクを実行します。

  1. SolverFactory を作成して、各データセットの Solver を構築します。
  2. データセットをロードします。
  3. Solver.solve() で解決します。
  4. そのデータセットの解を視覚化します。

通常、アプリケーションには、解決する問題データセットごとに新しい Solver インスタンスを構築するための SolverFactory が 1 つあります。SolverFactory はスレッドセーフですが、Solver はそうではありません。学校の時間割アプリケーションの場合、データセットは 1 つしかないため、Solver インスタンスは 1 つだけです。

完成した TimeTableApp.java クラスは次のとおりです。

package org.acme.schooltimetabling;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.acme.schooltimetabling.domain.Lesson;
import org.acme.schooltimetabling.domain.Room;
import org.acme.schooltimetabling.domain.TimeTable;
import org.acme.schooltimetabling.domain.Timeslot;
import org.acme.schooltimetabling.solver.TimeTableConstraintProvider;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.config.solver.SolverConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TimeTableApp {

    private static final Logger LOGGER = LoggerFactory.getLogger(TimeTableApp.class);

    public static void main(String[] args) {
        SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig()
                .withSolutionClass(TimeTable.class)
                .withEntityClasses(Lesson.class)
                .withConstraintProviderClass(TimeTableConstraintProvider.class)
                // The solver runs only for 5 seconds on this small data set.
                // It's recommended to run for at least 5 minutes ("5m") otherwise.
                .withTerminationSpentLimit(Duration.ofSeconds(5)));

        // Load the problem
        TimeTable problem = generateDemoData();

        // Solve the problem
        Solver<TimeTable> solver = solverFactory.buildSolver();
        TimeTable solution = solver.solve(problem);

        // Visualize the solution
        printTimetable(solution);
    }

    public static TimeTable generateDemoData() {
        List<Timeslot> timeslotList = new ArrayList<>(10);
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));

        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));

        List<Room> roomList = new ArrayList<>(3);
        roomList.add(new Room("Room A"));
        roomList.add(new Room("Room B"));
        roomList.add(new Room("Room C"));

        List<Lesson> lessonList = new ArrayList<>();
        long id = 0;
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade"));
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade"));
        lessonList.add(new Lesson(id++, "Physics", "M. Curie", "9th grade"));
        lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "9th grade"));
        lessonList.add(new Lesson(id++, "Biology", "C. Darwin", "9th grade"));
        lessonList.add(new Lesson(id++, "History", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade"));
        lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade"));

        lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
        lessonList.add(new Lesson(id++, "Physics", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(id++, "French", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(id++, "Geography", "C. Darwin", "10th grade"));
        lessonList.add(new Lesson(id++, "History", "I. Jones", "10th grade"));
        lessonList.add(new Lesson(id++, "English", "P. Cruz", "10th grade"));
        lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "10th grade"));

        return new TimeTable(timeslotList, roomList, lessonList);
    }

    private static void printTimetable(TimeTable timeTable) {
        LOGGER.info("");
        List<Room> roomList = timeTable.getRoomList();
        List<Lesson> lessonList = timeTable.getLessonList();
        Map<Timeslot, Map<Room, List<Lesson>>> lessonMap = lessonList.stream()
                .filter(lesson -> lesson.getTimeslot() != null && lesson.getRoom() != null)
                .collect(Collectors.groupingBy(Lesson::getTimeslot, Collectors.groupingBy(Lesson::getRoom)));
        LOGGER.info("|            | " + roomList.stream()
                .map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |");
        LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
        for (Timeslot timeslot : timeTable.getTimeslotList()) {
            List<List<Lesson>> cellList = roomList.stream()
                    .map(room -> {
                        Map<Room, List<Lesson>> byRoomMap = lessonMap.get(timeslot);
                        if (byRoomMap == null) {
                            return Collections.<Lesson>emptyList();
                        }
                        List<Lesson> cellLessonList = byRoomMap.get(room);
                        if (cellLessonList == null) {
                            return Collections.<Lesson>emptyList();
                        }
                        return cellLessonList;
                    })
                    .collect(Collectors.toList());

            LOGGER.info("| " + String.format("%-10s",
                    timeslot.getDayOfWeek().toString().substring(0, 3) + " " + timeslot.getStartTime()) + " | "
                    + cellList.stream().map(cellLessonList -> String.format("%-10s",
                            cellLessonList.stream().map(Lesson::getSubject).collect(Collectors.joining(", "))))
                            .collect(Collectors.joining(" | "))
                    + " |");
            LOGGER.info("|            | "
                    + cellList.stream().map(cellLessonList -> String.format("%-10s",
                            cellLessonList.stream().map(Lesson::getTeacher).collect(Collectors.joining(", "))))
                            .collect(Collectors.joining(" | "))
                    + " |");
            LOGGER.info("|            | "
                    + cellList.stream().map(cellLessonList -> String.format("%-10s",
                            cellLessonList.stream().map(Lesson::getStudentGroup).collect(Collectors.joining(", "))))
                            .collect(Collectors.joining(" | "))
                    + " |");
            LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
        }
        List<Lesson> unassignedLessons = lessonList.stream()
                .filter(lesson -> lesson.getTimeslot() == null || lesson.getRoom() == null)
                .collect(Collectors.toList());
        if (!unassignedLessons.isEmpty()) {
            LOGGER.info("");
            LOGGER.info("Unassigned lessons");
            for (Lesson lesson : unassignedLessons) {
                LOGGER.info("  " + lesson.getSubject() + " - " + lesson.getTeacher() + " - " + lesson.getStudentGroup());
            }
        }
    }

}

main () メソッドは最初に SolverFactory を作成します。

SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig()
        .withSolutionClass(TimeTable.class)
        .withEntityClasses(Lesson.class)
        .withConstraintProviderClass(TimeTableConstraintProvider.class)
        // The solver runs only for 5 seconds on this small data set.
        // It's recommended to run for at least 5 minutes ("5m") otherwise.
        .withTerminationSpentLimit(Duration.ofSeconds(5)));

SolverFactory の作成により、以前に作成した @PlanningSolution クラス、@PlanningEntity クラス、および ConstraintProvider クラスが登録されます。

終了設定または terminationEarly() イベントがない場合、ソルバーは永久に実行されます。これを避けるために、ソルバーは解決時間を 5 秒に制限します。

5 秒後、main() メソッドは問題をロードして解決し、解決策を出力します。

        // Load the problem
        TimeTable problem = generateDemoData();

        // Solve the problem
        Solver<TimeTable> solver = solverFactory.buildSolver();
        TimeTable solution = solver.solve(problem);

        // Visualize the solution
        printTimetable(solution);

solve() メソッドはすぐには戻りません。最適解を返す前に 5 秒間実行されます。

OptaPlanner は、利用可能な終了時間内に見つかった最適なソリューションを返します。NP 困難な問題の性質上、特に大規模なデータセットの場合、最適解が最適ではない可能性があります。終了時間を増やして、より良い解決策を見つけてください。

generateDemoData() メソッドは、解決する学校の時間割の問題を生成します。

printTimetable() メソッドは時刻表をコンソールにきれいに出力するので、スケジュールが適切かどうかを視覚的に簡単に判断できます。

14.6. 学校の時間割アプリケーションの作成と実行

学校の時間割 Java アプリケーションのすべてのコンポーネントが完成したので、それらをすべて TimeTableApp.java クラスにまとめて実行する準備が整いました。

前提条件

  • 学校の時間割アプリケーションに必要なすべてのコンポーネントを作成しました。

手順

  1. src/main/java/org/acme/schooltimetabling/TimeTableApp.java クラスを作成します。

    package org.acme.schooltimetabling;
    
    import java.time.DayOfWeek;
    import java.time.Duration;
    import java.time.LocalTime;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import org.acme.schooltimetabling.domain.Lesson;
    import org.acme.schooltimetabling.domain.Room;
    import org.acme.schooltimetabling.domain.TimeTable;
    import org.acme.schooltimetabling.domain.Timeslot;
    import org.acme.schooltimetabling.solver.TimeTableConstraintProvider;
    import org.optaplanner.core.api.solver.Solver;
    import org.optaplanner.core.api.solver.SolverFactory;
    import org.optaplanner.core.config.solver.SolverConfig;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class TimeTableApp {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(TimeTableApp.class);
    
        public static void main(String[] args) {
            SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig()
                    .withSolutionClass(TimeTable.class)
                    .withEntityClasses(Lesson.class)
                    .withConstraintProviderClass(TimeTableConstraintProvider.class)
                    // The solver runs only for 5 seconds on this small data set.
                    // It's recommended to run for at least 5 minutes ("5m") otherwise.
                    .withTerminationSpentLimit(Duration.ofSeconds(5)));
    
            // Load the problem
            TimeTable problem = generateDemoData();
    
            // Solve the problem
            Solver<TimeTable> solver = solverFactory.buildSolver();
            TimeTable solution = solver.solve(problem);
    
            // Visualize the solution
            printTimetable(solution);
        }
    
        public static TimeTable generateDemoData() {
            List<Timeslot> timeslotList = new ArrayList<>(10);
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));
    
            timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));
    
            List<Room> roomList = new ArrayList<>(3);
            roomList.add(new Room("Room A"));
            roomList.add(new Room("Room B"));
            roomList.add(new Room("Room C"));
    
            List<Lesson> lessonList = new ArrayList<>();
            long id = 0;
            lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade"));
            lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade"));
            lessonList.add(new Lesson(id++, "Physics", "M. Curie", "9th grade"));
            lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "9th grade"));
            lessonList.add(new Lesson(id++, "Biology", "C. Darwin", "9th grade"));
            lessonList.add(new Lesson(id++, "History", "I. Jones", "9th grade"));
            lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade"));
            lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade"));
            lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade"));
            lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade"));
    
            lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
            lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
            lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
            lessonList.add(new Lesson(id++, "Physics", "M. Curie", "10th grade"));
            lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "10th grade"));
            lessonList.add(new Lesson(id++, "French", "M. Curie", "10th grade"));
            lessonList.add(new Lesson(id++, "Geography", "C. Darwin", "10th grade"));
            lessonList.add(new Lesson(id++, "History", "I. Jones", "10th grade"));
            lessonList.add(new Lesson(id++, "English", "P. Cruz", "10th grade"));
            lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "10th grade"));
    
            return new TimeTable(timeslotList, roomList, lessonList);
        }
    
        private static void printTimetable(TimeTable timeTable) {
            LOGGER.info("");
            List<Room> roomList = timeTable.getRoomList();
            List<Lesson> lessonList = timeTable.getLessonList();
            Map<Timeslot, Map<Room, List<Lesson>>> lessonMap = lessonList.stream()
                    .filter(lesson -> lesson.getTimeslot() != null && lesson.getRoom() != null)
                    .collect(Collectors.groupingBy(Lesson::getTimeslot, Collectors.groupingBy(Lesson::getRoom)));
            LOGGER.info("|            | " + roomList.stream()
                    .map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |");
            LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
            for (Timeslot timeslot : timeTable.getTimeslotList()) {
                List<List<Lesson>> cellList = roomList.stream()
                        .map(room -> {
                            Map<Room, List<Lesson>> byRoomMap = lessonMap.get(timeslot);
                            if (byRoomMap == null) {
                                return Collections.<Lesson>emptyList();
                            }
                            List<Lesson> cellLessonList = byRoomMap.get(room);
                            if (cellLessonList == null) {
                                return Collections.<Lesson>emptyList();
                            }
                            return cellLessonList;
                        })
                        .collect(Collectors.toList());
    
                LOGGER.info("| " + String.format("%-10s",
                        timeslot.getDayOfWeek().toString().substring(0, 3) + " " + timeslot.getStartTime()) + " | "
                        + cellList.stream().map(cellLessonList -> String.format("%-10s",
                                cellLessonList.stream().map(Lesson::getSubject).collect(Collectors.joining(", "))))
                                .collect(Collectors.joining(" | "))
                        + " |");
                LOGGER.info("|            | "
                        + cellList.stream().map(cellLessonList -> String.format("%-10s",
                                cellLessonList.stream().map(Lesson::getTeacher).collect(Collectors.joining(", "))))
                                .collect(Collectors.joining(" | "))
                        + " |");
                LOGGER.info("|            | "
                        + cellList.stream().map(cellLessonList -> String.format("%-10s",
                                cellLessonList.stream().map(Lesson::getStudentGroup).collect(Collectors.joining(", "))))
                                .collect(Collectors.joining(" | "))
                        + " |");
                LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
            }
            List<Lesson> unassignedLessons = lessonList.stream()
                    .filter(lesson -> lesson.getTimeslot() == null || lesson.getRoom() == null)
                    .collect(Collectors.toList());
            if (!unassignedLessons.isEmpty()) {
                LOGGER.info("");
                LOGGER.info("Unassigned lessons");
                for (Lesson lesson : unassignedLessons) {
                    LOGGER.info("  " + lesson.getSubject() + " - " + lesson.getTeacher() + " - " + lesson.getStudentGroup());
                }
            }
        }
    
    }
  2. TimeTableApp クラスを通常の Java アプリケーションのメインクラスとして実行します。次の出力が得られるはずです。

    ...
    INFO  |            | Room A     | Room B     | Room C     |
    INFO  |------------|------------|------------|------------|
    INFO  | MON 08:30  | English    | Math       |            |
    INFO  |            | I. Jones   | A. Turing  |            |
    INFO  |            | 9th grade  | 10th grade |            |
    INFO  |------------|------------|------------|------------|
    INFO  | MON 09:30  | History    | Physics    |            |
    INFO  |            | I. Jones   | M. Curie   |            |
    INFO  |            | 9th grade  | 10th grade |            |
    ...
  3. コンソール出力を確認します。すべての厳しい制約に準拠していますか?TimeTableConstraintProviderroomConflict 制約をコメントアウトするとどうなりますか?

info ログは、OptaPlanner がその 5 秒間に何をしたかを示しています。

... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4).
... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398).
... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE).

14.7. アプリケーションのテスト

適切なアプリケーションにはテストが含まれます。timetable プロジェクトで制約とソルバーをテストします。

14.7.1. 学校の時間割の制約をテストする

timetable プロジェクトの各制約を個別にテストするには、単体テストで ConstraintVerifier を使用します。これにより、各制約のコーナーケースが他のテストから分離されてテストされるため、適切なテストカバレッジで新しい制約を追加する際のメンテナンスが軽減されます。

このテストは、制約 TimeTableConstraintProvider::roomConflict が、同じ部屋で 3 つのレッスンを与えられ、そのうちの 2 つのレッスンが同じタイムスロットを持つ場合、一致の重み 1 でペナルティを課すことを検証します。したがって、制約の重みが 10hard の場合、スコアは -10hard 減少します。

手順

src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java クラスを作成します。

package org.acme.optaplanner.solver;

import java.time.DayOfWeek;
import java.time.LocalTime;

import javax.inject.Inject;

import io.quarkus.test.junit.QuarkusTest;
import org.acme.optaplanner.domain.Lesson;
import org.acme.optaplanner.domain.Room;
import org.acme.optaplanner.domain.TimeTable;
import org.acme.optaplanner.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;

@QuarkusTest
class TimeTableConstraintProviderTest {

    private static final Room ROOM = new Room("Room1");
    private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON);
    private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON);

    @Inject
    ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier;

    @Test
    void roomConflict() {
        Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1");
        Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2");
        Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3");

        firstLesson.setRoom(ROOM);
        firstLesson.setTimeslot(TIMESLOT1);

        conflictingLesson.setRoom(ROOM);
        conflictingLesson.setTimeslot(TIMESLOT1);

        nonConflictingLesson.setRoom(ROOM);
        nonConflictingLesson.setTimeslot(TIMESLOT2);

        constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
                .given(firstLesson, conflictingLesson, nonConflictingLesson)
                .penalizesBy(1);
    }

}

制約の重みが ConstraintProvider でハードコーディングされている場合でも、ConstraintVerifier がテスト中に制約の重みを無視することに注意してください。これは、実稼動に入る前に制約の重みが定期的に変更されるためです。このように、制約の重みの微調整によって単体テストが中断されることはありません。

14.7.2. 学校の時間割ソルバーをテストする

以下の例では、Red Hat build of Quarkus で Red Hat build of OptaPlanner の時間割プロジェクトをテストします。このアプリケーションは、JUnit テストを使用してテストのデータセットを生成し、TimeTableController に送信して解決します。

手順

  1. 以下の内容を含む src/test/java/com/example/rest/TimeTableResourceTest.java クラスを作成します。

    package com.exmaple.optaplanner.rest;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.inject.Inject;
    
    import io.quarkus.test.junit.QuarkusTest;
    import com.exmaple.optaplanner.domain.Room;
    import com.exmaple.optaplanner.domain.Timeslot;
    import com.exmaple.optaplanner.domain.Lesson;
    import com.exmaple.optaplanner.domain.TimeTable;
    import com.exmaple.optaplanner.rest.TimeTableResource;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @QuarkusTest
    public class TimeTableResourceTest {
    
        @Inject
        TimeTableResource timeTableResource;
    
        @Test
        @Timeout(600_000)
        public void solve() {
            TimeTable problem = generateProblem();
            TimeTable solution = timeTableResource.solve(problem);
            assertFalse(solution.getLessonList().isEmpty());
            for (Lesson lesson : solution.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(solution.getScore().isFeasible());
        }
    
        private TimeTable generateProblem() {
            List<Timeslot> timeslotList = new ArrayList<>();
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
            timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));
    
            List<Room> roomList = new ArrayList<>();
            roomList.add(new Room("Room A"));
            roomList.add(new Room("Room B"));
            roomList.add(new Room("Room C"));
    
            List<Lesson> lessonList = new ArrayList<>();
            lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade"));
            lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade"));
            lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade"));
            lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade"));
            lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade"));
    
            lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade"));
            lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade"));
            lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade"));
            lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade"));
            lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade"));
            return new TimeTable(timeslotList, roomList, lessonList);
        }
    
    }

    このテストは、解決後にすべての授業がタイムスロットと部屋に割り当てられていることを確認します。また、実行可能解 (ハード制約の違反なし) も確認します。

  2. テストプロパティーを src / main / resources /application.properties ファイルに追加します。

    # The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
    # It's recommended to run for at least 5 minutes ("5m") otherwise.
    quarkus.optaplanner.solver.termination.spent-limit=5s
    
    # Effectively disable this termination in favor of the best-score-limit
    %test.quarkus.optaplanner.solver.termination.spent-limit=1h
    %test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*soft

通常、ソルバーは 200 ミリ秒未満で実行可能解を検索します。application.properties が、実行可能なソリューション (0hard/*soft) が見つかると同時に終了するように、テスト中のソルバーの終了を上書きします。こうすることで、ユニットテストが任意のハードウェアで実行される可能性があるため、ソルバーの時間をハードコード化するのを回避します。このアプローチを使用することで、動きが遅いシステムであっても、実行可能なソリューションを検索するのに十分な時間だけテストが実行されます。ただし、高速システムでも、厳密に必要とされる時間よりもミリ秒単位で長く実行されることはありません。

14.8. ロギング

Red Hat build of OptaPlanner の時間割プロジェクトを完了後にロギング情報を使用すると、ConstraintProvider で制約が微調整しやすくなります。info ログファイルでスコア計算の速度を確認して、制約に加えた変更の影響を評価します。デバッグモードでアプリケーションを実行して、アプリケーションが行う手順をすべて表示するか、追跡ログを使用して全手順および動きをロギングします。

手順

  1. 時間割アプリケーションを一定の時間 (例: 5 分) 実行します。
  2. 以下の例のように、log ファイルのスコア計算の速度を確認します。

    ... Solving ended: ..., score calculation speed (29455/sec), ...
  3. 制約を変更して、同じ時間、プランニングアプリケーションを実行し、log ファイルに記録されているスコア計算速度を確認します。
  4. アプリケーションをデバッグモードで実行して、アプリケーションの全実行ステップをログに記録します。

    • コマンドラインからデバッグモードを実行するには、-D システムプロパティーを使用します。
    • デバッグモードを永続的に有効にするには、以下の行を application.properties ファイルに追加します。

      quarkus.log.category."org.optaplanner".level=debug

      以下の例では、デバッグモードでの log ファイルの出力を表示します。

      ... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
      ...     CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]).
      ...     CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]).
      ...
  5. trace ロギングを使用して、全手順、および手順ごとの全動きを表示します。

14.9. Micrometer と Prometheus を使用して学校の時間割を監視する OptaPlanner Java アプリケーション

OptaPlanner は、Java アプリケーション用のメトリック計測ライブラリーである Micrometer を介してメトリックを公開します。Prometheus で Micrometer を使用して、学校の時間割アプリケーションで OptaPlanner ソルバーを監視できます。

前提条件

  • OptaPlanner 学校の時間割アプリケーションを Java で作成しました。
  • Prometheus がインストールされている。Prometheus のインストールについては、Prometheus の Web サイトを参照してください。

手順

  1. Micrometer Prometheus 依存関係を学校の時間割 pom.xml ファイルに追加します。<MICROMETER_VERSION> は、インストールした Micrometer のバージョンです。

    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-prometheus</artifactId>
     <version><MICROMETER_VERSION></version>
    </dependency>
    注記

    micrometer-core 依存関係も必要です。ただし、この依存関係は optaplanner-core 依存関係に含まれているため、pom.xml ファイルに追加する必要はありません。

  2. 次の import ステートメントを TimeTableApp.java クラスに追加します。

    import io.micrometer.core.instrument.Metrics;
    import io.micrometer.prometheus.PrometheusConfig;
    import io.micrometer.prometheus.PrometheusMeterRegistry;
  3. TimeTableApp.java クラスのメインメソッドの先頭に次の行を追加して、ソリューションが開始する前に Prometheus が com.sun.net.httpserver.HttpServer からデータを破棄できるようにします。

    PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    
            try {
                HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
                server.createContext("/prometheus", httpExchange -> {
                    String response = prometheusRegistry.scrape();
                    httpExchange.sendResponseHeaders(200, response.getBytes().length);
                    try (OutputStream os = httpExchange.getResponseBody()) {
                        os.write(response.getBytes());
                    }
                });
    
                new Thread(server::start).start();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    
            Metrics.addRegistry(prometheusRegistry);
    
            solve();
        }
  4. 次の行を追加して、解決時間を制御します。解決時間を調整することで、解決に費やされた時間に基づいて指標がどのように変化するかを確認できます。

    withTerminationSpentLimit(Duration.ofMinutes(5)));
  5. 学校の時間割アプリケーションを開始します。
  6. Web ブラウザーで http://localhost:8080/prometheus を開き、Prometheus で timetable アプリケーションを表示します。
  7. 監視システムを開いて、OptaPlanner プロジェクトのメトリックを表示します。

    次のメトリックが公開されます。

    • optaplanner_solver_errors_total: 測定開始以降に解決中に発生したエラーの総数。
    • optaplanner_solver_solve_duration_seconds_active_count: 現在解決しているソルバーの数。
    • optaplanner_solver_solve_duration_seconds_max: 現在アクティブなソルバーの実行時間が最も長い実行時間。
    • optaplanner_solver_solve_duration_seconds_duration_sum: アクティブな各ソルバーの解決時間の合計。たとえば、アクティブなソルバーが 2 つあり、一方が 3 分間実行され、もう一方が 1 分間実行されている場合、合計計算時間は 4 分です。

パート V. Red Hat build of OptaPlanner の配送経路プランニングスターターアプリケーションのデプロイおよび使用

開発者は、OptaWeb 配送経路スターターアプリケーションを使用して、車両での配送を最適化できます。

前提条件

  • OpenJDK (JDK) 11 がインストールされている。Red Hat ビルドの Open JDK は Red Hat カスマーポータル (ログインが必要) の ソフトウェアダウンロード ページから入手できます。
  • Apache Maven 3.6 以降がインストールされている。Maven は Apache Maven Project の Web サイトから入手できます。

第15章 OptaWeb 配送経路

多くのビジネスは、各種貨物を輸送することを主な目的としています。これらのビジネスでは積荷の地点から目的地まで貨物を運送し、最も効率的な方法で車両を使用することを目指しています。主な目的の 1 つは、時間または距離のいずれかで測定される通過コストを最小限に抑えることです。

この種類の最適化問題は、運搬経路問題 (VRP: Vehicle Routing Problem) と呼ばれており、さまざまなバリエーションがあります。

Red Hat build of OptaPlanner は、配送経路のバリエーションを多数解決して、ソリューションの例を提供します。OptaPlanner を使用すると、開発者は 制約プログラミング 理論を学習するのではなく、ビジネスルールや要件のモデル化に焦点を当てることができます。OptaWeb 配送経路では、以下のような質問に解答するスターターアプリケーションを提供することで OptaPlanner の配送経路機能を拡張します。

  • 距離と移動時間はどこから取得しますか ?
  • ソリューションをマップ上で視覚化するにはどうすればよいですか ?
  • クラウドで実行するアプリケーションを構築するにはどうすればよいですか ?

OptaWeb 配送経路は OpenStreetMap (OSM) データファイルを使用します。OpenStreetMap の詳細は、OpenStreetMap の Web サイトを参照してください。

OptaWeb 配送経路を使用する場合は、次の定義を使用してください。

地域: OSM ファイルで表現される、地球上の地図の任意エリア。地域は、国、都市、大陸、頻繁にまとめて使用される複数の国などです。たとえば、DACH の地域にはドイツ (DE)、オーストリア (AT)、およびスイス (CH) が含まれます。

国コード: ISO-3166 標準により割り当てられた 2 文字のコード。国コードを使用して地理検索の結果を絞り込みます。複数の国にまたがる地域 (例: DACH 地域) を使用する場合があるため、OptaWeb 配送経路はこのような地域で地理検索の絞り込みができるように、国コードの一覧も使用できます。国コードの一覧については、ISO 3166 Country Codes を参照してください。

地理検索: 検索キーワードとして地域の住所や場所名を指定して、GPS の場所番号を結果として受け取るクエリーの種類。検索キーワードの一意性により、返される場所の数は異なります。大抵の場合、場所の名前は一意ではないため、作業地域の (複数の) 国の場所だけを含めることで、関連のない結果を除外します。

第16章 OptaWeb 配送経路デプロイメントファイルのダウンロードおよびビルド

デプロイメントファイルをダウンロードし、準備してから、OptaWeb 配送経路の構築、デプロイを行う必要があります。

手順

  1. Red Hat カスタマーポータルの Software Downloads ページに移動し (ログインが必要)、ドロップダウンオプションから製品およびバージョンを選択します。

    • 製品: Red Hat build of OptaPlanner
    • バージョン: 8.33
  2. Red Hat build of OptaPlanner 8.33 クイックスタート をダウンロードします。
  3. rhbop-8.33.0-optaplanner-quickstarts-sources.zip ファイルを展開します。
  4. org.optaweb.optaweb-vehicle-routing-8.33.0.Final-redhat-00004 ディレクトリーに移動します。
  5. OptaPlanner 8.33.0 Maven Repositroy の Red Hat ビルド (rhbop-8.33.0-optaplanner-maven-repository.zip) をダウンロードします。
  6. rhbop-8.33.0-optaplanner-maven-repository.zip ファイルを展開します。
  7. rhbop-8.33.0-optaplanner/maven-repository サブディレクトリーの内容を ~/.m2/repository ディレクトリーにコピーします。
  8. 以下のコマンドを入力して OptaWeb 配送経路をビルドします。

    mvn clean package -DskipTests

第17章 runLocally.sh スクリプトを使用してローカルで OptaWeb 配送経路を実行します。

Linux を使用する場合は、runLocally.sh の Bash スクリプトを使用して OptaWeb 配送経路を実行できます。

注記

runLocally.sh スクリプトは、MacOS では実行されません。runLocally.sh スクリプトを使用できない場合は、18章OptaWeb 配送経路の手動での設定および実行 を参照してください。

runLocally.sh スクリプトは、以下の設定手順を自動化しますが、このスクリプトを使用しない場合は、この設定を手動で実行する必要があります。

  • データディレクトリーを作成します。
  • Geofabrik から選択した OpenStreetMap (OSM) ファイルをダウンロードします。
  • ダウンロードした各 OSM ファイルに国コードを自動的に関連付けてみます。
  • スタンドアロン JAR ファイルが存在しない場合は、プロジェクトをビルドします。
  • OptaWeb 配送経路を起動するには、地域の引数を 1 つ指定するか、対話的に地域を選択します。

runLocally.sh スクリプトの実行に関する説明は、以下のセクションを参照してください。

17.1. クイックスタートモードで OptaWeb 配送経路の runLocally.sh スクリプトを実行します。

最も簡単な方法で OptaWeb 配送経路を使い始めるには、引数を指定せずに runLocally.sh スクリプトを実行します。

前提条件

手順

  1. org.optaweb.optaweb-vehicle-routing-8.33.0.Final-redhat-00004 ディレクトリーで次のコマンドを入力します。

     ./runLocally.sh
  2. .optaweb-vehicle-routing ディレクトリーを作成するようにプロンプトが表示されたら、y と入力します。スクリプトの初回実行時に、このディレクトリーを作成するようにプロンプトが表示されます。
  3. OSM ファイルのダウンロードのプロンプトが表示された場合は、y と入力します。このスクリプトの初回実行時に、OptaWeb 配送経路が Belgium OSM ファイルをダウンロードします。

    アプリケーションは、OSM ファイルのダウンロード後に起動します。

  4. OptaWeb 配送経路のユーザーインターフェイスを表示するには、Web ブラウザーに以下の URL を入力します。

    http://localhost:8080
注記

このスクリプトを初回実行する場合は、GraphHopper で OSM ファイルをインポートして道路網のグラフとして保存する必要があるため、起動に数分かかります。次回から runlocally.sh スクリプトを実行する場合、読み込み時間ははるかに短縮されます。

17.2. OptaWeb 配送経路の runLocally.sh スクリプトを対話モードで実行

対話モードを使用して、ダウンロードした OSM ファイルと、各地域に割り当てられた国コードの一覧を表示します。対話モードを使用すると、Web サイトに移動してダウンロード先を選択せずに Geofabrik から追加の OSM ファイルをダウンロードできます。

前提条件

手順

  1. org.optaweb.optaweb-vehicle-routing-8.33.0.Final-redhat-00004 ディレクトリーに移動します。
  2. 以下のコマンドを入力して対話モードでスクリプトを実行します。

    ./runLocally.sh -i
  3. Your choice のプロンプトで、d を入力してダウンロードメニューを表示します。以前にダウンロードした地域の一覧と、その後にダウンロード可能な地域の一覧が表示されます。
  4. 任意: 以前にダウンロードした地域の一覧から地域を選択します。

    1. ダウンロードした地域の一覧で地域に関連付けられた番号を入力します。
    2. Enter キーを押します。
  5. 任意: 地域をダウンロードします。

    1. ダウンロードする地域に関連付けられている番号を入力します。たとえば、ヨーロッパの地図を選択するには、5 と入力します。
    2. 地図をダウンロードするには、d と入力して、Enter キーを押します。
    3. 地図内の特定の地域をダウンロードするには、e と入力して、ダウンロードする地域に関連付けられている番号を入力して、Enter キーを押します。

      サイズの大きい OSM ファイルの使用

      欧州の個々の国や、アメリカの州など、小さい地域を使用すると、最適なユーザーエクスペリエンスが得られます。1 GB 以上の OSM ファイルを使用するには、かなりのメモリーサイズを必要とし、初期処理に時間がかかります (最大では数時間)。

      アプリケーションは、OSM ファイルのダウンロード後に起動します。

  6. OptaWeb 配送経路のユーザーインターフェイスを表示するには、Web ブラウザーに以下の URL を入力します。

    http://localhost:8080

17.3. OptaWeb 配送経路の runLocally.sh スクリプトを非対話モードで実行

非対話モードで OptaWeb 配送経路を使用して、コマンド 1 つで OptaWeb 配送経路を起動し、先ほどダウンロードした OSM ファイルを追加します。これは、地域間を素早く切り替える場合や、デモを行う場合に便利です。

前提条件

手順

  1. org.optaweb.optaweb-vehicle-routing-8.33.0.Final-redhat-00004 ディレクトリーに移動します。
  2. 以下のコマンドを実行します。ここでは、<OSM_FILE_NAME> は先ほどダウンロードした OSM ファイルに置き換えます。

    ./runLocally.sh <OSM_FILE_NAME>

17.4. データディレクトリーの更新

別のデータディレクトリーを使用する場合は、OptaWeb 配送経路が使用するディレクトリーとは異なるデータディレクトリーに更新できます。デフォルトのデータディレクトリーは $HOME/.optaweb-vehicle-routing です。

前提条件

手順

  • 別のデータディレクトリーを使用するには、現在のデータディレクトリーに、.DATA_DIR_LAST ファイルへのディレクトリーの絶対パスを指定します。
  • 地域に関連付けられている国コードを変更するには、現在のデータディレクトリーの country_codes ディレクトリーにある対応のファイルを編集します。

    たとえば、スコットランドの OSM ファイルをダウンロードし、スクリプトで国コードを推測できなかった場合に、country_codes/scotland-latest のコンテンツを GB に設定します。

  • リージョンを削除するには、データディレクトリーの openstreetmap ディレクトリーにある対応の OSM ファイルを削除し、graphhopper ディレクトリーで地域のディレクトリーを削除します。

第18章 OptaWeb 配送経路の手動での設定および実行

OptaWeb 配送経路を最も簡単な方法で実行するには、runlocally.sh スクリプトを使用します。ただし、お使いのシステムでバッシュを利用できない場合は、runlocally.sh スクリプトが実行する手順を手動で完了してください。

前提条件

手順

  1. 経路データをダウンロードします。

    経路エンジンでは、車両が場所間の移動にかかる時間を計算するのに地理データが必要です。ローカルのファイルシステムに OpenStreetMap (OSM) データファイルをダウンロードして保存してから、OptaWeb 配送経路を実行する必要があります。

    注記

    OSM データファイルのサイズは通常 100 MB から 1 GB の間となり、ダウンロードに時間がかかるため、OptaWeb 配送経路アプリケーションをビルドまたは起動する前にこれらのファイルをダウンロードすることをお勧めします。

    1. Web ブラウザーで http://download.geofabrik.de/ を開きます。
    2. Sub Region リストで地域 (ヨーロッパ など) をクリックします。サブ地域ページが開きます。
    3. Sub Regions の表で、国 (ベルギーなど) の OSM ファイル (.osm.pbf) をダウンロードします。
  2. データのディレクトリー構造を作成します。

    OptaWeb 配送経路では、ファイルシステム上の複数の種類のデータを読み取り、書き込みます。openstreetmap ディレクトリーから OSM (OpenStreetMap) ファイルを読み取り、道路網グラフを graphhopper ディレクトリーに書き込み、db ディレクトリーでユーザーデータを永続化します。このデータをすべて格納する専用のディレクトリーを新たに作成して、今後簡単に新しいバージョンの OptaWeb 配送経路にアップグレードして、以前に作成したデータをそのまま使用できるようにします。

    1. $HOME/.optaweb-vehicle-routing ディレクトリーを作成します。
    2. $HOME/.optaweb-vehicle-routing ディレクトリーに openstreetmap ディレクトリーを作成します。

      $HOME/.optaweb-vehicle-routing
      └── openstreetmap
    3. ダウンロードした OSM ファイル (.osm.pbf の拡張子が付いたファイル) をすべて openstreetmap ディレクトリーに移動します。

      残りのディレクトリー構造は、OptaWeb 配送経路アプリケーションにより、初回実行時に作成されます。作成後のディレクトリー構造は以下のとおりです。

      $HOME/.optaweb-vehicle-routing
      
      ├── db
      │   └── vrp.mv.db
      ├── graphhopper
      │   └── belgium-latest
      └── openstreetmap
          └── belgium-latest.osm.pbf
  3. rhbop-8.33.0-kogito-and-optaplanner-quickstarts/optaweb-8.33.0.Final-redhat-00004/optaweb-vehicle-routing/optaweb-vehicle-routing-standalone/target ディレクトリーに移動します。
  4. OptaWeb 配送経路を実行するには、以下のコマンドを実行します。

    java \
    -Dapp.demo.data-set-dir=$HOME/.optaweb-vehicle-routing/dataset \
    -Dapp.persistence.h2-dir=$HOME/.optaweb-vehicle-routing/db \
    -Dapp.routing.gh-dir=$HOME/.optaweb-vehicle-routing/graphhopper \
    -Dapp.routing.osm-dir=$HOME/.optaweb-vehicle-routing/openstreetmap \
    -Dapp.routing.osm-file=<OSM_FILE_NAME> \
    -Dapp.region.country-codes=<COUNTRY_CODE_LIST> \
    -jar quarkus-app/quarkus-run.jar

    このコマンドでは、以下の変数を置き換えてください。

    • <OSM_FILE_NAME>: 以前にダウンロードした地域で、使用予定の地域の OSM ファイル。
    • <COUNTRY_CODE_LIST>: 地理検索クエリーの絞り込みに使用するコンマ区切りの国コード一覧。国コードの一覧については、ISO 3166 Country Codes を参照してください。

      アプリケーションは、OSM ファイルのダウンロード後に起動します。

      以下の例では、OptaWeb 配送経路は中央アメリカの OSM の地図 (central-america-latest.osm.pbf) をダウンロードして、ベリーズ (BZ) とグアテマラ (GT) の国で検索を行います。

      java \
      -Dapp.demo.data-set-dir=$HOME/.optaweb-vehicle-routing/dataset \
      -Dapp.persistence.h2-dir=$HOME/.optaweb-vehicle-routing/db \
      -Dapp.routing.gh-dir=$HOME/.optaweb-vehicle-routing/graphhopper \
      -Dapp.routing.osm-dir=$HOME/.optaweb-vehicle-routing/openstreetmap \
      -Dapp.routing.osm-file=entral-america-latest.osm.pbf \
      -Dapp.region.country-codes=BZ,GT \
      -jar quarkus-app/quarkus-run.jar
  5. OptaWeb 配送経路のユーザーインターフェイスを表示するには、Web ブラウザーに以下の URL を入力します。

    http://localhost:8080

第19章 Red Hat OpenShift で OptaWeb 車両ルーティングの実行

Linux を使用する場合には、Bash スクリプト runOnOpenShift.sh を使用して、Red Hat OpenShift に OptaWeb 配送経路をインストールできます。

注記

MacOS では runOnOpenShift.sh スクリプトは実行されません。

前提条件

手順

  1. Red Hat OpenShift クラスターにログインするか、クラスターを起動します。
  1. 以下のコマンドを入力します。<PROJECT_NAME> は新規プロジェクト名に置き換えます。

    oc new-project <PROJECT_NAME>
  2. 必要に応じて、org.optaweb.optaweb-vehicle-routing-8.33.0.Final-redhat-00004 ディレクトリーに移動します。
  3. 以下のコマンドを入力して、runOnOpenShift.sh スクリプトを実行し、OpenStreetMap (OSM) ファイルをダウンロードします。

    ./runOnOpenShift.sh <OSM_FILE_NAME> <COUNTRY_CODE_LIST> <OSM_FILE_DOWNLOAD_URL>

    このコマンドでは、以下の変数を置き換えてください。

    • <OSM_FILE_NAME>: <OSM_FILE_DOWNLOAD_URL> からダウンロードしたファイルの名前。
    • <COUNTRY_CODE_LIST>: 地理検索クエリーの絞り込みに使用するコンマ区切りの国コード一覧。国コードの一覧については、ISO 3166 Country Codes を参照してください。
    • <OSM_FILE_DOWNLOAD_URL>: OpenShift からアクセス可能な、PBF 形式の OSM データの URL。このファイルは、バックエンドの起動中にダウンロードされ、/deployments/local/<OSM_FILE_NAME> として保存されます。

      以下の例では、OptaWeb 配送経路は中央アメリカの OSM の地図 (central-america-latest.osm.pbf) をダウンロードして、ベリーズ (BZ) とグアテマラ (GT) の国で検索を行います。

      ./runOnOpenShift.sh central-america-latest.osm.pbf BZ,GT http://download.geofabrik.de/europe/central-america-latest.osm.pbf
注記

runOnOpenShift.sh スクリプトのヘルプを参照するには、./runOnOpenShift.sh --help と入力します。

19.1. デプロイされた OptaWeb 配送経路アプリケーションをローカル変更で更新

OptaWeb Vehicle Routing アプリケーションを Red Hat OpenShift にデプロイしたら、バックエンドとフロントエンドを更新できます。

前提条件

  • OptaWeb 配送経路が Maven で正常にビルドされ、OpenShift にデプロイされている。

手順

  • バックエンドを更新するには、次の手順を実行します。

    1. ソースコードを変更し、Maven でバックエンドモジュールをビルドします。
    2. org.optaweb.optaweb-vehicle-routing-8.33.0.Final-redhat-00004 ディレクトリーに移動します。
    3. 以下のコマンドを入力して、OpenShift ビルドを起動します。

      oc start-build backend --from-dir=. --follow
  • フロントエンドを更新するには、次の手順を実行します。

    1. ソースコードを変更して、npm ユーティリティーでフロントエンドモジュールをビルドします。
    2. sources/optaweb-vehicle-routing-frontend ディレクトリーに移動します。
    3. 以下のコマンドを入力して、OpenShift ビルドを起動します。

      oc start-build frontend --from-dir=docker --follow

第20章 OptaWeb 配送経路の使用

OptaWeb 配送経路アプリケーションでは、地図に場所の数をマークできます。最初の場所はデポ (配送拠点) であることを前提とします。車両はこのデポから、マークの付いた他のすべての場所に商品を配送する必要があります。

車両の数、および全車両の積載容量を設定できますが、経路では全車両を使用する保証はありません。ただし、そのルートがすべての車両に使用されるとは限りません。アプリケーションは、最適なルートに必要な数だけ車両を使用します。

現在のバージョンには、一定の制限があります。

  • 1 つの場所に配送するたびに、車両の積載量が 1 ポイント消費されます。たとえば、積載量 が 10 の車両は、デポに戻るまでに最大 10 箇所まで訪問できます。
  • 車両や場所にカスタム名を設定することはできません。

20.1. 経路の作成

最適な経路を作成するには、OptaWeb 配送経路ユーザーインターフェイスの Demo タブを使用します。

前提条件

  • OptaWeb 配送経路が実行されており、ユーザーインターフェイスにアクセスできる。

手順

  1. OptaWeb 配送経路では、Demo をクリックして Demo タブを開きます。
  2. 地図上の青いプラス/マイナスボタンを使用して車両数を設定します。デフォルトでは、車両ごとの積載量は 10 となっています。
  3. 必要に応じて、地図上の四角の中にあるプラスボタンを使用して、拡大します。

    注記

    地図の拡大にダブルクリックを使用しないでください。ダブルクリックすると、場所が作成されます。

  4. デポの場所をクリックします。
  5. 配送ポイントについては、地図の他の場所をクリックします。
  6. 場所を削除する場合:

    1. 削除する場所の上のマウスをかざして、場所の名前を表示します。
    2. 画面の左側にある一覧でその場所名を検索します。
    3. 名前の横にある X アイコンをクリックします。

場所を追加/削除したり、車両数を変更したりするたびに、アプリケーションは新しい最適経路を作成して表示します。そのソリューションで複数の車両を使用する場合、アプリケーションは、全車両の経路を別の色で表示します。

20.2. その他の情報の表示と設定

OptaWeb 配送経路の他のタブを使用し、追加の情報を表示して設定できます。

前提条件

  • OptaWeb 配送経路が実行されており、ユーザーインターフェイスにアクセスできる。

手順

  • Vehicles タブをクリックして、車両の表示、追加、削除や、全車両の積載量の設定が可能です。
  • Visits タブをクリックし、場所を表示して削除します。
  • Route タブをクリックして、各車両を選択して、選択した車両の経路を表示します。

20.3. OptaWeb 配送経路でのカスタムデータセットの作成

ベルギー内の複数の大都市を含むデモデータセットが組み込まれています。Load demo メニューで他のデモを利用する場合は、ご自身のデータセットを用意します。

手順

  1. OptaWeb 配送経路で、地図をクリックするか、地理検索を使用して、デポ 1 つと、訪問数 1 つ以上を追加しますします。
  2. Export をクリックして、データセットディレクトリーにファイルを保存します。

    注記

    データセットディレクトリーは、app.demo.data-set-dir プロパティーで指定したディレクトリーです。

    アプリケーションが runLocally.sh スクリプト経由で実行されている場合は、データセットディレクトリーが $HOME/.optaweb-vehicle-routing/dataset に設定されます。

    それ以外の場合は、プロパティー application.properties ファイルから取得し、rhbop-8.33.0-kogito-and-optaplanner-quickstarts/optaweb-8.33.0.Final-redhat-00004/optaweb-vehicle-routing/optaweb-vehicle-routing-standalone/target/local/dataset にデフォルト設定されます。

    app.demo.data-set-dir プロパティーを編集して、別のデータディレクトリーを指定できます。

  3. YAML ファイルを編集して、データセットの一意名を選択します。
  4. バックエンドを再起動します。

バックエンドを再起動した後に、Load demo メニューにデータセットディレクトリーのファイルが表示されます。

20.4. OptaWeb 配送経路のトラブルシューティング

OptaWeb 配送経路で予期せぬ動作をする場合は、以下の手順に従い、トラブルシューティングを行います。

前提条件

  • OptaWeb 配送経路は実行されるが、予期せぬ動作をする。

手順

  1. 問題を特定するには、バックエンドの端末出力ログを確認します。
  2. 問題を解決するには、バックエンドデータベースを削除します。

    1. バックエンドの端末ウィンドウで Ctrl+C を押して、バックエンドを停止します。
    2. optaweb-vehicle-routing/optaweb-vehicle-routing-backend/local/db ディレクトリーを削除します。
    3. OptaWeb 配送経路を再起動します。

第21章 OptaWeb 配送経路の開発ガイド

本セクションでは、開発モードでバックエンドモジュールおよびフロントエンドモジュールを設定して実行する方法を説明します。

21.1. OptaWeb 配送経路プロジェクトの構造

OptaWeb 配送経路プロジェクトは、マルチモジュールの Maven プロジェクトです。

図21.1 モジュール依存関係ツリーの図

バックエンドとフロントエンドモジュールは、モジュールツリーの一番下にあります。これらのモジュールには、アプリケーションのソースコードが含まれます。

スタンドアロンモジュールは、バックエンドとフロントエンドを 1 つの実行可能な JAR ファイルに統合するアセンブリーモジュールです。

ディストリビューションモジュールは、最終的なアセンブリーの手順を表します。このモジュールは、スタンドアロンアプリケーションと、ドキュメントを受けとり、配信しやすいようにアーカイブとしてラッピングします。

バックエンドとフロントエンドは、別にビルドとデプロイが可能な、別個のプロジェクトです。実際には、全く異なる言語で記述され、異なるツールでビルドされています。どちらのプロジェクトでも、コードの変更からアプリケーションの実行までの対応時間を短くし、最新の開発体験ができるようにするツールが含まれています。

次のセクションでは、バックエンドとフロントエンドのプロジェクトを開発モードで実行する方法を説明します。

21.2. OptaWeb 配送経路のバックエンドモジュール

バックエンドモジュールには、Red Hat build of OptaPlanner を使用して配送経路を最適化するサーバー側のアプリケーションが含まれています。最適化は CPU を集中的に使用する計算であり、最大限に能力を発揮するには I/O 操作を回避する必要があります。移動コスト (時間または距離) を最小限に抑えることが主な目的の 1 つであるため、OptaWeb 配送経路では RAM メモリーに移動コストの情報を保持します。OptaPlanner は、解を出す時に、ユーザーが入力した全場所間の移動コストを把握しておく必要があります。この情報は、距離行列 と呼ばれる構造に保存されます。

新しい場所を入力すると、OptaWeb 配送経路は新しい場所と、それ以外でこれまでに入力されたすべての場所との間の移動コストを計算して、距離行列にその移動コストを保存します。移動コストの計算は、GraphHopper の経路エンジンで実行されます。

バックエンドモジュールは、以下のような機能を追加で実装します。

  • 永続性
  • フロントエンドの WebSocket 接続
  • データセットの読み込み、エクスポート、およびインポート

バックエンドコードのアーキテクチャーの詳細は、22章OptaWeb 配送経路のバックエンドアーキテクチャー を参照してください。

次のセクションでは、開発モードでバックエンドを設定して実行する方法を説明します。

21.2.1. OptaWeb 配送経路のバックエンドモジュールの実行

バックエンドモジュールは、Quarkus 開発モードで実行できます。

前提条件

手順

  1. rhbop-8.33.0-kogito-and-optaplanner-quickstarts/optaweb-8.33.0.Final-redhat-00004/optaweb-vehicle-routing/optaweb-vehicle-routing-backend ディレクトリーに移動します。
  2. 開発モードでバックエンドを実行するには、次のコマンドを入力します。

    mvn compile quarkus:dev

21.2.2. IntelliJ IDEA Ultimate からの OptaWeb 配送経路バックエンドモジュールの実行

IntelliJ IDEA Ultimate を使用して OptaWeb 配送経路バックエンドモジュールを実行し、プロジェクトの開発を簡素化できます。IntelliJ IDEA Ultimate には、Quarkus プラグインが同梱されており、Quarkus フレームワークを使用するモジュールの実行設定を自動的に作成します。

手順

optaweb-vehicle-routing-backend の設定を使用して、バックエンドを実行します。

関連情報

詳細は、Quarkus アプリケーションのビルド を参照してください。

21.2.3. Quarkus 開発モード

開発モードでは、バックエンドのソースコードまたは設定が変更された場合に、フロントエンドが実行されるブラウザータブを更新すると、バックエンドが自動的に再起動します。

Quarkus 開発モード の詳細を確認してください。

21.2.4. OptaWeb 配送経路バックエンドモジュールのシステムプロパティーの値の変更

OptaWeb 配送経路バックエンドモジュールのデフォルトのシステムプロパティー値を一時的または永続的に上書きできます。

OptaWeb 配送経路バックエンドモジュールのプロパティーは /src/main/resources/application.properties ファイルに保存されます。このファイルはバージョン管理されます。このファイルを使用してデフォルトの設定プロパティーの値を永続的に保存し、Quarkus プロファイルを定義します。

前提条件

手順

  • デフォルトのシステムプロパティー値を一時的に上書きするには、mvn または java コマンドの実行時に、-D<PROPERTY>=<VALUE> 引数を追加します。<PROPERTY> は変更するプロパティーの名前、<VALUE> はそのプロパティーに一時的に割り当てる値に置き換えます。以下の例は、Maven を使用して dev モードで Quarkus プロジェクトをコンパイルした場合に、quarkus.http.port システムプロパティーの値を 8181 に一時的に変更する方法を示しています。

    mvn compile quarkus:dev -Dquarkus.http.port=8181

    これを実行すると、/src/main/resources/application.properties ファイルに保存されているプロパティーの値が一時的に変更されます。

  • 開発環境に固有の設定を保存する場合など設定値を永続的に変更するには、env-example ファイルの内容を optaweb-vehicle-routing-backend/.env ファイルにコピーします。

    このファイルはバージョン管理には含まれないので、リポジトリーのクローン時には存在しません。Git の作業ツリーに影響を与えずに、.env ファイルで変更を加えることができます。

関連情報

OptaWeb 配送経路の設定プロパティーに関する完全一覧は、23章OptaWeb 配送経路のバックエンド設定プロパティー を参照してください。

21.2.5. OptaWeb 配送経路のバックエンドログ

OptaWeb 配送経路は、SLF4J API と Logback をロギングフレームワークとして使用します。詳細は、Quarkus のロギングの設定 を参照してください。

21.3. OptaWeb 配送経路のフロントエンドモジュールの操作

フロントエンドのプロジェクトは、Create React App でブートストラップされました。Create React App には、開発や、実稼働環境でアプリケーションをビルドしやすくするためのスクリプトや依存関係が多数含まれています。

前提条件

手順

  1. Fedora で、次のコマンドを入力して開発環境を設定します。

    sudo dnf install npm

    npm のインストールの詳細については、Downloading and installing Node.js and npm を参照してください。

  2. rhbop-8.33.0-kogito-and-optaplanner-quickstarts/optaweb-8.33.0.Final-redhat-00004/optaweb-vehicle-routing/optaweb-vehicle-routing-frontend ディレクトリーに移動します。
  3. npm の依存関係をインストールします。

    npm install

    Maven とは違い、npm パッケージマネージャーは、npm install を実行した場合にのみ、プロジェクトディレクトリーの node_modules に依存関係をインストールします。package.json にリストされている依存関係が変更されると、master ブランチに変更をプルした時点で、npm install を実行してから開発サーバーを実行する必要があります。

  4. 以下のコマンドを入力して、開発サーバーを実行します。

    npm start
  5. 自動的に表示されない場合には、Web ブラウザーで http://localhost:3000/ を開きます。

    デフォルトでは npm start コマンドは、デフォルトのブラウザーでこの URL を開こうとします。

    注記

    npm start コマンドで、実行するたびに新規ブラウザーのタブを開かないようにするには、BROWSER=none 環境変数をエクスポートします。.env.local ファイルを使用して、この設定を永続化します。これには、以下のコマンドを実行します。

    echo BROWSER=none >> .env.local

    ブラウザーは、フロントエンドのソースコードを変更するたびにページを更新します。端末で実行する開発サーバーのプロセスは、これらの変更を取得し、コンパイルエラーと lint エラーをコンソールに出力します。

  6. 以下のコマンドを入力して、テストを実行します。

    npm test
  7. REACT_APP_BACKEND_URL 環境変数の値を変更して、npm startnpm run build などの実行時に、npm が使用するバックエンドプロジェクトの場所を指定します。

    REACT_APP_BACKEND_URL=http://10.0.0.123:8081
    注記

    環境変数は、npm のビルドプロセス中は JavaScript バンドル内でハードコード化されるため、バックエンドの場所を指定してから、フロントエンドをビルドしてデプロイする必要があります。

    React 環境変数の詳細は、Adding Custom Environment Variables を参照してください。

  8. フロントエンドをビルドするには、以下のコマンドのいずれか 1 つを実行します。

    ./mvnw install
    mvn install

第22章 OptaWeb 配送経路のバックエンドアーキテクチャー

ドメインモデルおよびユースケースは、アプリケーションには必要不可欠です。OptaWeb 配送経路ドメインモデルは、アーキテクチャーの中心にあり、その周りにユースケースを埋め込むアプリケーション層があります。経路最適化、距離計算、永続化、ネットワーク通信などの機能は実装の詳細とみなされ、アーキテクチャーの一番外側に配置されます。

図22.1 アプリケーション層の図

22.1. コードの編成

以前の図で示されるように、バックエンドコードは 3 つの層で整理されます。

org.optaweb.vehiclerouting
├── domain
├── plugin          # Infrastructure layer
│   ├── persistence
│   ├── planner
│   ├── routing
│   └── rest
└── service         # Application layer
    ├── demo
    ├── distance
    ├── error
    ├── location
    ├── region
    ├── reload
    ├── route
    └── vehicle

service パッケージには、ユースケースを実装するアプリケーション層が含まれます。plugin パッケージにはインフラストラクチャー層が含まれます。

各層のコードは、さらに機能別に編成されます。つまり、各サービスまたはプラグインに独自のパッケージがあります。

22.2. 依存関係ルール

コンパイル時間の依存関係は、外層から中心に向けてのみ可能です。このルールに従うことで、ドメインモデルを、基盤となるフレームワークや、他の実装詳細から独立させ、ビジネスエンティティーの動作をより正確にモデル化できます。プレゼンテーションや永続性を周辺に押し出すことで、ビジネスエンティティーとユースケースの動作をより簡単にテストできます。

ドメインには依存関係はありません。

サービスはドメインにだけ依存します。サービスが (データベースまたはクライアントに) 結果を送信する必要がある場合には、出力境界インターフェイスを使用します。実装は contexts and dependency injection (CDI) コンテナーで注入されます。

プラグインは、2 つの方法でサービスに依存します。1 つ目は、ユーザー入力や最適化エンジンによる経路の更新など、イベントを基にサービスを呼び出します。サービスがプラグインに注入され、構築や依存関係の解決の負荷を IoC コンテナーに移動します。2 つ目は、プラグインがサービス出力境界インターフェイスを実装し、変更を永続化してデータベースに保存したり、応答を Web UI に送信したりなど、ユースケースの結果を処理します。

22.3. ドメインパッケージ

domain パッケージには、business objects が含まれており、LocationVehicleRoute など、このプロジェクトのドメインをモデル化します。このようなオブジェクトは完全にビジネス指向で、オブジェクトリレーションマッピングツールや Web サービスフレームワークなど、ツールやフレームワークの影響を受けないようにする必要があります。

22.4. サービスパッケージ

service パッケージには、ユースケース を実装するクラスが含まれます。ユースケースには、新しい場所の追加、車両の積載量の変更、住所の座標検索など、実行することを記述します。ユースケースを統括するビジネスルールは、ドメインオブジェクトを使用して表現します。

サービスは、永続性、Web、最適化など、外層のプラグインを操作する必要があります。層と層の間の依存関係ルールを満たすには、サービスの依存関係を定義するインターフェイスという観点で、サービスとプラグインの間のやり取りを表現します。プラグインは、サービスの境界インターフェイスを実装する Bean を指定して、サービスの依存関係を満たすことができます。CDI コンテナーは、プラグイン Bean のインスタンスを作成し、ランタイム時にサービスに注入します。これは、制御原理の反転例です。

22.5. プラグインパッケージ

plugin パッケージには、最適化、永続性、経路、ネットワーク通信などのインフラストラクチャー機能が含まれます。

第23章 OptaWeb 配送経路のバックエンド設定プロパティー

以下の表に記載されている OptaWeb 配送経路アプリケーションプロパティーを設定できます。

プロパティータイプ説明

app.demo.data-set-dir

相対パスまたは絶対パス

/home/user/.optaweb-vehicle-routing/dataset

カスタムデータセットは、このディレクトリーから読み込まれます。デフォルトは、local/dataset です。

app.persistence.h2-dir

相対パスまたは絶対パス

/home/user/.optaweb-vehicle-routing/db

データベースファイルの保存に H2 が使用するディレクトリー。デフォルトは local/db です。

app.region.country-codes

ISO 3166-1 alpha-2 国コードの一覧

USGB,IE, DE,AT,CH。空白でも構いません。

地理検索結果を制限します。

app.routing.engine

列挙

air, graphhopper

経路エンジンの実装。デフォルトは graphhopper です。

app.routing.gh-dir

相対パスまたは絶対パス

/home/user/.optaweb-vehicle-routing/graphhopper

道路網グラフの保存に GraphHopper が使用するディレクトリー。デフォルトは local/graphhopper です。

app.routing.osm-dir

相対パスまたは絶対パス

/home/user/.optaweb-vehicle-routing/openstreetmap

OSM ファイルを含むディレクトリー。デフォルトは local/openstreetmap です。

app.routing.osm-file

ファイル名

belgium-latest.osm.pbf

GraphHopper が読み込む OSM ファイル名。ファイルは app.routing.osm-dir に配置する必要があります。

optaplanner.solver.termination.spent-limit

java.time.Duration

  • 1m
  • 150s
  • P2dT21h (PnDTnHnMn.nS)

場所の変更後にソルバーを実行する時間。

server.address

IP アドレスまたはホスト名

10.0.0.123, my-vrp.geo-1.openshiftapps.com

サーバーをバインドするネットワークアドレス。

server.port

ポート番号

4000, 8081

サーバーの HTTP ポート。

付録A バージョン情報

本書の最終更新日: 2023 年 3 月 14 日 (火)

法律上の通知

Copyright © 2023 Red Hat, Inc.
The text of and illustrations in this document are licensed by Red Hat under a Creative Commons Attribution–Share Alike 3.0 Unported license ("CC-BY-SA"). An explanation of CC-BY-SA is available at http://creativecommons.org/licenses/by-sa/3.0/. In accordance with CC-BY-SA, if you distribute this document or an adaptation of it, you must provide the URL for the original version.
Red Hat, as the licensor of this document, waives the right to enforce, and agrees not to assert, Section 4d of CC-BY-SA to the fullest extent permitted by applicable law.
Red Hat, Red Hat Enterprise Linux, the Shadowman logo, the Red Hat logo, JBoss, OpenShift, Fedora, the Infinity logo, and RHCE are trademarks of Red Hat, Inc., registered in the United States and other countries.
Linux® is the registered trademark of Linus Torvalds in the United States and other countries.
Java® is a registered trademark of Oracle and/or its affiliates.
XFS® is a trademark of Silicon Graphics International Corp. or its subsidiaries in the United States and/or other countries.
MySQL® is a registered trademark of MySQL AB in the United States, the European Union and other countries.
Node.js® is an official trademark of Joyent. Red Hat is not formally related to or endorsed by the official Joyent Node.js open source or commercial project.
The OpenStack® Word Mark and OpenStack logo are either registered trademarks/service marks or trademarks/service marks of the OpenStack Foundation, in the United States and other countries and are used with the OpenStack Foundation's permission. We are not affiliated with, endorsed or sponsored by the OpenStack Foundation, or the OpenStack community.
All other trademarks are the property of their respective owners.
Red Hat logoGithubRedditYoutubeTwitter

詳細情報

試用、購入および販売

コミュニティー

Red Hat ドキュメントについて

Red Hat をお使いのお客様が、信頼できるコンテンツが含まれている製品やサービスを活用することで、イノベーションを行い、目標を達成できるようにします。

多様性を受け入れるオープンソースの強化

Red Hat では、コード、ドキュメント、Web プロパティーにおける配慮に欠ける用語の置き換えに取り組んでいます。このような変更は、段階的に実施される予定です。詳細情報: Red Hat ブログ.

会社概要

Red Hat は、企業がコアとなるデータセンターからネットワークエッジに至るまで、各種プラットフォームや環境全体で作業を簡素化できるように、強化されたソリューションを提供しています。

© 2024 Red Hat, Inc.