第15章 反復計画
15.1. 反復計画の概要
世の中は常に変化しています。解 (ソリューション) を作成するのに使用する問題ファクトは、その解の実行前または実行中に変更できます。状況は複数あり、組み合わせ使用することもできます。
- 突発的なファクトの変更: シフトに入っている従業員が病気で休むと電話してきた、離陸が予定されている飛行機に技術的問題が発生して遅れている、機械または車両が壊れたなどの状況が発生した場合は、バックアップ計画 を使用します。
- 現時点ですべてのエンティティーを割り当てられない: 一部を割り当てないままにしておきます。たとえば、同時に割り当てられるシフト数が 10 個あっても、割り当てられる従業員が 9 人しかいない場合は、過制約計画 を使用します。
- 長期的未来の未知のファクト: たとえば、今後 2 週間の入院予定は確実ですが、3、4 週間後の予定になると確実ではなくなり、5 週間後以降はいま計画しても役に立ちません。継続的計画 を使用します。
- 常に変化する問題ファクト: リアルタイム計画 を使用します。
問題ファクトが変化するリスクを減らすために、計画の開始を遅らせるのは、あまり良い方法ではありません。CPU 時間を増やすことがプランニングソリューションを良くします。計画が不完全な方が、全くないよりも良くなります。
このため、最適化アルゴリズムでは、すでに (一部) 計画されている解の計画をサポートしています。これは反復計画と呼ばれています。
15.2. バックアップ計画
バックアップ計画は、何か問題が発生したときのために、計画に余裕を作る余分なスコア制約を追加する技術です。これにより、計画の中にバックアップ計画が作成されます。たとえば、従業員を予備の従業員として割り当て (同じ時間の 10 個のシフトに対して 1 つの予備)、病院のベッドを、各科に 1 つ空いたままにしておくなどです。
これにより、問題が発生したとき (従業員の 1 人が病気で電話してきた場合) に、元の解の問題ファクトを変更し (病気の従業員を削除し、そのシフトを割り当てないままにして)、計画を再度行い、スコアが変わったその解から始めます。構築ヒューリスティックは、新たに作成されたギャップを埋め (おそらく予備の従業員)、メタヒューリスティックがそれを今後さらに改善します。
15.3. 過剰制限計画
すべてのプランニングエンティティーを割り当てるのに適したソリューションがない場合は、ハード制約に違反せずに割り当てられるだけのエンティティーを割り当てることが必要になります。これは、過剰制限計画と呼ばれます。
これは、以下のように実装します。
- ScoreDefinition を変えて、余分なスコアレベル (通常は、ハードレベルとソフトレベルとの間にある中間レベル) を追加します。
- プランニング変数 nullable を作成します。
- 新しいレベルにスコア制約 (通常は中間制約) を追加し、割り当てていないエンティティーにペナルティーを追加します (または、その合計に重みを追加します)。
15.4. 継続的計画 (ウィンドウがある計画)
継続的計画は、同時に 1 つ以上の計画ウィンドウを、毎月、毎週、毎日、毎時に、そのプロセスを繰り返し計画するテクニックです。時間は無制限であるため、将来のウィンドウは無限になり、そのウィンドウをすべて計画するのは不可能です。したがって、一定数のウィンドウだけを計画します。
過去の計画ウィンドウは変えられません。次に行う計画ウィンドウは安定してる (変更する可能性が低い) と見なされ、その後で発生するウィンドウはドラフトと見なされます (次の計画時に変更する可能性があります)。すぐに発生しない計画ウィンドウは何も計画されていません。
過去の計画ウィンドウには、固定の 計画ウィンドウだけがあります。プランニングエンティティーは変更できなくなります (Move できないため) が、次回のプランニングエンティティーで適用するスコア制約の一部に影響する可能性があるため、その一部はスコア計算で必要になります。たとえば、従業員は、連続して 6 日以上働くことができない条件で、すでに 4 日連続して働いている場合は、本日と翌日のいずれかを休みにする必要があります。
すべてのプランニングエンティティーが常に変更できないわけではありません。変更することはできますが、元のものと異なる場合は、スコアペナルティーが発生します。たとえば、患者の入院開始日の 2 日前以降は (本当に必要な場合を除いて) 病床を再スケジュールしない、搭乗手続きの 2 時間前以降は飛行機の乗降口を変更しないなどです。
最初の計画の 11 月 1 日と、新しい計画の 11 月 5 日の違いに注目してください。問題ファクトの一部 (F、H、I、J、K) が変更したため、関係ないプランニングエンティティー (G) も変更しました。
15.4.1. 動かせないプランニングエンティティー
プランニングエンティティーを動かせないようにするには、SelectionFilter
エンティティーを追加します。これは、エンティティーを動かせる場合は true
を、動かせない場合は false
を返します。
public class MovableShiftAssignmentSelectionFilter implements SelectionFilter<ShiftAssignment> { public boolean accept(ScoreDirector scoreDirector, ShiftAssignment shiftAssignment) { ShiftDate shiftDate = shiftAssignment.getShift().getShiftDate(); NurseRoster nurseRoster = (NurseRoster) scoreDirector.getWorkingSolution(); return nurseRoster.getNurseRosterInfo().isInPlanningWindow(shiftDate); } }
設定は以下のようになります。
@PlanningEntity(movableEntitySelectionFilter = MovableShiftAssignmentSelectionFilter.class) public class ShiftAssignment { ... }
カスタムの MoveListFactory
および MoveIteratorFactory
の実装では、移動できないエンティティーを移動させないようにする必要があります。
15.4.2. 中断を最小限に抑える長期的な再計画 (完全には動かせないプランニングエンティティー)
既存の計画を置き換えると、その計画が大きく中断する可能性があります。その計画が人 (従業員、ドライバーなど) に影響する場合、大抵は中断は望ましくありません。このような場合は、長期的な再計画が役に立ちます。計画を変更する利点が、それによって中断が発生するよりも大きくなります。
たとえば、マシンの再割当て例では、エンティティーにプランニング変数 machine
と、その元の値 originalMachine
があります。
@PlanningEntity(...) public class ProcessAssignment { private MrProcess process; private Machine originalMachine; private Machine machine; public Machine getOriginalMachine() {...} @PlanningVariable(...) public Machine getMachine() {...} public boolean isMoved() { return originalMachine != null && originalMachine != machine; } ... }
計画時、プランニング変数 machine
が変更します。それを originalMachine と比較することで、計画における変更にペナルティーが追加されます。
rule "processMoved" when ProcessAssignment(moved == true) then scoreHolder.addSoftConstraintMatch(kcontext, -1000); end
ソフトペナルティー -1000
は、変更した各変数で、ソフトスコアが 1000
ポイント以上改善する場合 (またはハードスコアが改善する場合) は、最適解だけが許可されることを示しています。
15.5. リアルタイム計画
リアルタイム計画を行うには、最初に バックアップ計画 と 継続的計画 を短期の計画ウィンドウと組み合わせて、リアルタイム計画の負荷を下げます。時間が過ぎると、問題が変化します。
上の例では、元々 07:55
に解決が終了するように顧客が設定されていたり、車が出発した後などに、顧客を 3 人、異なる時間 (07:56
、08:02
、および 08:45
) に追加しました。Planner は、(移動できないプランニングエンティティー とともに) ProblemFactChange
を使用したこのようなシナリオを処理します。
15.5.1. ProblemFactChange
Solver
が解決中に、外部イベントにより問題ファクトを変更する場合があります。たとえば、飛行機が遅れたために、滑走路の使用を遅らせる必要がある場合です。(別のスレッドから、もしくは同じスレッドでも) Solver
を使用して解決中に問題ファクトのインスタンスを変更しないでください。変更すると壊れます。代わりに、ProblemFactChange
を Solver
に追加して、Solver スレッドをできるだけ早く実行します。
public interface Solver { ... boolean addProblemFactChange(ProblemFactChange problemFactChange); boolean isEveryProblemFactChangeProcessed(); ... }
public interface ProblemFactChange { void doChange(ScoreDirector scoreDirector); }
以下は例になります。
public void deleteComputer(final CloudComputer computer) { solver.addProblemFactChange(new ProblemFactChange() { public void doChange(ScoreDirector scoreDirector) { CloudBalance cloudBalance = (CloudBalance) scoreDirector.getWorkingSolution(); // First remove the problem fact from all planning entities that use it for (CloudProcess process : cloudBalance.getProcessList()) { if (ObjectUtils.equals(process.getComputer(), computer)) { scoreDirector.beforeVariableChanged(process, "computer"); process.setComputer(null); scoreDirector.afterVariableChanged(process, "computer"); } } // A SolutionCloner does not clone problem fact lists (such as computerList) // Shallow clone the computerList so only workingSolution is affected, not bestSolution or guiSolution cloudBalance.setComputerList(new ArrayList<CloudComputer>(cloudBalance.getComputerList())); // Next remove it the problem fact itself for (Iterator<CloudComputer> it = cloudBalance.getComputerList().iterator(); it.hasNext(); ) { CloudComputer workingComputer = it.next(); if (ObjectUtils.equals(workingComputer, computer)) { scoreDirector.beforeProblemFactRemoved(workingComputer); it.remove(); // remove from list scoreDirector.afterProblemFactRemoved(workingComputer); break; } } } }); }
ProblemFactChange
の問題ファクトまたはプランニングエンティティーに加えた変更は、ScoreDirector
に通知する必要があります。
ProblemFactChange
を正しく記述するには、計画のクローンの振る舞いを正しく理解している必要があります。
-
ProblemFactChange
における変更は、scoreDirector.getWorkingSolution()
のSolution
インスタンスで行う必要があります。workingSolution
が、BestSolutionChangedEvent
のbestSolution
に対する 計画クローン です。したがって、Solver
のworkingSolution
は、残りのアプリケーションのSolution
と同じインスタンスにはなりません。 -
計画クローンは、プランニングエンティティーとプランニングエンティティーコレクションのクローンも作成します。プランニングエンティティーへの変更は、
scoreDirector.getWorkingSolution()
が保持するインスタンスで行う必要があります。 計画クローンは、問題ファクトのクローンでも、問題ファクトのコレクションでもありません。したがって、
workingSolution
とbestSolution
は、同じ問題ファクトインスタンスと、同じ問題ファクトリストインスタンスを共有します。ProblemFactChange
が変更した問題ファクトまたは問題ファクトリストは、最初にクローンを作成した問題です (これは、他の問題ファクトとプランニングエンティティーでルート変更参照を意味しています)。そうでない場合は、workingSolution
およびbestSolution
を異なるスレッド (たとえば Solver スレッドと GUI イベントスレッド) で使用する場合に、競合状態が発生します。
多くの変更では、プランニングエンティティーを初期化せず、部分的に初期化した解 (ソリューション) になります。最初の Solver フェーズで対応できれば問題ではありません。構築ヒューリスティックのすべての Solver フェーズでこれに対応できるため、最初のフェーズで、そのようなフェーズを設定することが推奨されます。
基本的に、Solver
が停止したら、ProblemFactChange
を実行して再起動します。これは、その初期解は 1 つ前の実行で調整した最適解になるため、ウォームスタート です。これは、構築ヒューリスティックが再度実行したことを示しますが、プランニング変数がほとんどもしくは全く初期化されないため (null 許容型のプランニング変数 がある場合を除きます)、それは、コールドスタートよりもはるかに速くなります。
(Solver およびフェーズ設定の両方に) 設定した各 終了
はリセットされ、terminateEarly()
への以前の呼び出しは未完成ではありません。ただし、通常、(デーモンモード以外で) 終了
を設定せず、結果が必要な場合のみ Solver.terminateEarly()
を呼び出します。代わりに、以下に示すように、終了
を設定し、BestSolutionChangedEvent
と組み合わせてデーモンモードを使用します。
15.5.2. デーモン: solve() Does Not Return
リアルタイム計画では、作業がなくなったときに Solverスレッドが待機し、問題ファクトが新たに変更すると問題解決がすぐに再開するのが便利なことが多いです。デーモンモードに Solver
を追加すると以下の効果があります。
Solver
の終了
が終了しても、solve()
からは返らず、そのスレッドをブロックします (これにより CPU パワーが解放されます)。-
solve()
からそれを返すterminateEarly()
を除いて、システムリソースを解放し、アプリケーションが適切にシャットダウンできるようになります。 -
Solver
が、空のプランニングエンティティーコレクションから開始する場合は、すぐにブロック状態で待機します。
-
-
ProblemFactChange
が追加されると、実行状態になり、ProblemFactChange
が適用され、Solver
を再度実行します。
デーモンモードを設定するには、以下のように設定します。
<solver> <daemon>true</daemon> ... </solver>
Solver スレッドが不自然に強制終了するのを避けるためにアプリケーションをシャットダウンする必要がある場合は、Solver.terminateEarly()
を呼び出す必要があります。
BestSolutionChangedEvent
をサブスクライブして、Solver スレッドが見つけた新しい最適解を処理します。BestSolutionChangedEvent
は、すべての ProblemFactChange
がすでに処理されていることを保証できず、その解が初期化され実行できる状態でもありません。そのような無効な解を持つ BestSolutionChangedEvents
を無視するには、以下を行います。
public void bestSolutionChanged(BestSolutionChangedEvent<CloudBalance> event) { // Ignore invalid solutions if (event.isEveryProblemFactChangeProcessed() && event.isNewBestSolutionInitialized() && event.getNewBestSolution().getScore().isFeasible()) { ... } }