時間槽模式 - Time slot
-
規劃實體中的規劃變量是一個時間區間;
-
一個規劃變量的取值最多僅可分配一個時間區間;
-
規劃變量對應的時間區間是等長的。
從圖中可以看出,每門課所需的時間都是固定一小時。具體到這個模式的應用,因爲其原理、結構和實現起來都相當簡單,本文不通過示例詳細講解了。可參考示例包中的Course timetabling中的設計和代碼。
時間粒模式 - Time Grain
-
規劃變量是時間區間;
-
業務上對應於規劃變量的時間區間可以不等長,但必須是Grain的倍數。
從上圖可以看到,每個會議所需的時間長度是不相等的,但是其長度必然是一個Time Grain的倍數,從圖中上方的時間刻度可以比劃出一個TimeGrain應該是15分鐘。例如Sales meeting佔用了4個Time Grain,即時長1小時。Time Grain模式的使用會相對Time Slot更靈活,適用範圍會更廣。通過設置可知,其實適用於Time Slot模型的情形,是完全可以通過TimeGrain模式實現的,只是實現起來會更復雜一些。那麼Time Grain模式的設計要點在哪裏呢?要了解其設計原理,就得先掌握Time Grain的結構及其對時間的提供方法。
-
設計好每個Grain的粒度,也就是時間長度。並不是粒度越細越好,例如以1秒鐘作爲一個粒度,是不是就可以將任務的時間精度控制在1級呢?理論上是可以的,但日常使用中不太可行。因爲這樣的設計會產生過量的Grain,Grain就是Value Range,當可選值的數量過多時,整個規劃問題的規模就會增大,其時間複雜度就會指數級上升,從而令優化效果降低。
-
定義好每個Grain與絕對時間的映射關係。這個模式中的Time Grain其時間上是相對的。如何理解呢?就是說,這個模式在運行的時候,會把初始化出來的Grain對象列表,以Index(Grain的序號)爲序形成一個連接的時間粒的序列。列表中每一個具體的Grain對應的絕對時間是什麼時候呢?是以第一個Grain作爲參照推算出來的。例如上圖中的第一個Grain - grain0它的起始時間是8:00, 那麼第6個grain - grain5的起始時間就是9:30,這個時間是通過grain0加上6個grain的時長推算出來的,也就是8:00加上1.5小時,因此得到的是9:30。因此,當你設定Time Grain與絕對時間的對應關係時,就需要從業務上考慮,grain0的起始是什麼時刻;它決定了後續所有任務的時間。
public int calculateOverlap(MeetingAssignment other) { if (startingTimeGrain == null || other.getStartingTimeGrain() == null) { return 0; }
int start = startingTimeGrain.getGrainIndex(); int end = start + meeting.getDurationInGrains(); int otherStart = other.startingTimeGrain.getGrainIndex(); int otherEnd = otherStart + other.meeting.getDurationInGrains(); if (end < otherStart) { return 0; } else if (otherEnd < start) { return 0; } return Math.min(end, otherEnd) - Math.max(start, otherStart); }
時間鏈模式 - Chained Through Time
Chained Through Time模式的意義
Chained Through Time的內存模型
-
一條鏈由一個Anchor(錨),和零或,或1個,或多個Entity(實體,其實就是規劃實體)構成;
-
一條鏈必須有且僅有一個Anchor(錨);
-
一條鏈中的Entity或Anchor之間是一對一的關係,不可出現合流或分流結構;
-
一條鏈中的Entity或Anchor不可出現循環。
Chained Through Time模式的設計實現
時間推算方法
Chained Through Time模式與其兩種時間規劃模式不同,本質上它並不對時間進行規劃,只對實體之間的關係進行規劃優化。因此,在引擎每一個原子操作中需要通過對VariableListener接口的實現,來對時間進行推算,並在完成推算後,由引擎通過評分機制進行約束評分。一個Move有可能對應多個原子操作,一個Move的操作種類,可以參見開發 手冊中關於Move Selector一章,在以後對引擎行爲進行深入分析的文章中,我將會寫一篇關於Move Seletor的文件,來揭示引擎的運行原理。在需要進行時間推算時,可以通過實現接口的afterVariableChanged方法,對當前所處理的規劃實體的時間進行更新。因爲Chained Through Timea模式下,所有已初始化的規劃實體都處在一條鏈上;因此,當一個規劃實體的時間被更新後,跟隨着它的後一個規劃實體的時間也需要被更新,如此類推,直到鏈上最後一個實體,或出現一個時間正好不需要更新的規劃實體,即該規劃實體前面的所有實體的時間出現更新後,其時間不用變化,那麼鏈上從它往後的規劃實體的時候也無需更新。
以下是VariableListener接口的afterVariableChanged及其處理方法。
// 實現VariableListener的類 public class StartTimeUpdatingVariableListener implements VariableListener<Task> { // 實現afterVariableChanged方法 @Override public void afterVariableChanged(ScoreDirector scoreDirector, Task task) { updateStartTime(scoreDirector, task); } @Override public void beforeEntityAdded(ScoreDirector scoreDirector, Task task) { // Do nothing } @Override public void afterEntityAdded(ScoreDirector scoreDirector, Task task) { updateStartTime(scoreDirector, task); } . . . }
//當一個任務的時候被更新時,順着鏈將它後面所有任務的時候都更新 protected void updateStartTime(ScoreDirector scoreDirector, Task sourceTask) { Step previous = sourceTask.getPreviousStep(); Task shadowTask = sourceTask; Integer previousEndTime = (previous == null ? null : previous.getEndTime()); Integer startTime = calculateStartTime(shadowTask, previousEndTime); while (shadowTask != null && !Objects.equals(shadowTask.getStartTime(), startTime)) { scoreDirector.beforeVariableChanged(shadowTask, "startTime"); shadowTask.setStartTime(startTime); scoreDirector.afterVariableChanged(shadowTask, "startTime"); previousEndTime = shadowTask.getEndTime(); shadowTask = shadowTask.getNextTask(); startTime = calculateStartTime(shadowTask, previousEndTime); } }
規劃實體的設計
上一步我們介紹瞭如何通過鏈在引擎的運行過程中進行時間推算,那麼如何設定才能讓引擎可以執行VariableListener中的方法呢,這就需要在規劃實體的設計過程中,反映出Chained Through Time的特性了。我們以上面的類圖爲例,理解下面其設計要求,在此示例中,把Task作爲規劃實體(Planning Entity), 那麼在Task類中需要定義一個Planning Variable(genuine planning variable), 它的類型是Step,它表示當前Task的上一個步驟(可能是另一個Task,也可能是一Machine). 此外,在 @PlanningVariable註解中,添加graphType = PlanningVariableGraphType.CHAINED說明。如下代碼:
// Planning variables: changes during planning, between score calculations. @PlanningVariable(valueRangeProviderRefs = {"machineRange", "taskRange"}, graphType = PlanningVariableGraphType.CHAINED) private Step previousStep;
以上代碼說明,規劃實體(Task)的genuine planning variable名爲previousStep, 它的Value Range有兩個來源,分別是機臺列表(machineRange)和任務列表(taskRange),並且添加了屬性grapType=planningVariableGraphType.CHAINED, 表明將應用Chained Through Time模式運行。
有了genuine planning variable, 還需要Shadow variable, 所謂的Shadow variable,在Chained Through Time模式下有兩種作用,分別是:
1. 用於建立兩個對象(Entity或Anchor)之間的又向依賴關係;即示例中的Machine與Task, 相信的兩個Task。
2. 用於指定當genuine planning variable的值在規劃運算過程產生變化時,需要更改哪個變量;即上面提到的開始時間。
,對於第一個作用,其代碼體現如下,在規劃實體(Task)中,以@AnchorShadowVariable註解,並在該註解的sourceVariableName中指定該Shadow Variable在鏈上的前一個對象指向的是哪個變量。
// Shadow variables // Task nextTask inherited from superclass @AnchorShadowVariable(sourceVariableName = "previousStep") private Machine machine;
上述代碼說明成員machine是一個Anchor Shadow Variable, 在鏈上,它連接的前一個實體是實體類的一個成員 - previousStep.
Chained Through Time中的鏈需要形成雙向關係(bi-directional),下圖是路線規劃示例中。一個客戶與上一個停靠點之間的又向關係。
在規劃實體(Task)中我們已經定義了前一個Step,並以@AnchorShadowVariable註解標識。而雙向關係中的另一方,則需要在相鄰節點中的前一個節點定義。通過鏈的內存模型,我們可以知道,在生產計劃示例中,一個實體的前一個節點的類型可能是另一個Task, 也要能是一個Machine, 因此,前一個節點指向後一個節點的規劃變量,只能在Task與Machine的共同父類中定義,也就是需要在Step中實現。因此,在Step類中需要定義另一個Shadow Variable, 因爲相對於Task中的Anchor Shadow variable, 它是反現的,因此,它需要通過@InverseRelationShadowVariable註解,說明它在鏈上起到反向連接作用,即它是指向後一個節點的。代碼如下:
@PlanningEntity public abstract class Step{ // Shadow variables @InverseRelationShadowVariable(sourceVariableName = "previousStep") protected Task nextTask; . . . }
可以從代碼中看到,Step類也是一個規劃實體.其中的一個成員nextTask, 它的類型是Task,它表示在鏈中指向後面的Entity. 大家可以想一下,爲什麼它可以是一個Task, 而無需是一個Step。
通過上述設計,已經實現了Chained Through Time的基本模式,可能大家還會問,上面我們實現了VariableListener, 引擎是如何觸發它的呢。這就需要用到另外一種Shadow Variable了,這種Shadow Varible是用於實現在運算過程中執行額外處理的,因此稱爲Custom Shadow Variable.
// 自定義Shadow Variable, 它表示當 genuine被引擎改變時,需要處理哪個變量。 @CustomShadowVariable(variableListenerClass = StartTimeUpdatingVariableListener.class, sources = {@PlanningVariableReference(variableName = "previousStep")}) private Integer startTime; // 因爲時間在規劃過程中以相對值進行運算,因此以整數表示。
上面的代碼通過@CustomShadowVariable註解,說明了Task的成員startTime是一個自定義的Shadow Variable. 同時在註解中添加了variableListenerClass屬性,其值指定爲剛纔我們定義的,實現了VariableListener接口的類 - StartTimeUpdatingVariableListener,同時,能冠軍sources屬性指定,當前Custom Shadow Variable是跟隨着genuine variable - previousStep的變化而變化的。
至此,關於Chained Through Time中的關鍵要點已全部設計實現,具體的使用可以參照示例包中有用到此模式的代碼。
總結
關於時間的規劃,在實際的系統開發時,並不只本文描述的那麼簡單,關於最爲複雜的Chained Through Time模式,大家可以通過本文了解其概念、結構和要點,再結合示例包中的代碼進來理解,才能掌握其要領。且現實項目中也有許許多多的個性規則和要求,需要通過大家的技巧來實現;但萬變不離其宗,所有處理特殊情況的技巧,都需要甚至Optaplanner這些既有特性。因此,大家可以先通過示例包中的代碼將這些特性掌握,再進行更復雜情況下的設計開如。未來若時間允許,我將分享我在項目中遇到的一些特殊,甚至是苛刻的規則要求,及其處理辦法。
如需瞭解更多關於Optaplanner的應用,請發電郵致:[email protected]
或到討論組發表你的意見:https://groups.google.com/forum/#!forum/optaplanner-cn
若有需要可添加本人微信(13631823503)或QQ(12977379)實時溝通,但因本人日常工作繁忙,通過微信,QQ等工具可能無法深入溝通,較複雜的問題,建議以郵件或討論組方式提出。(討論組屬於google郵件列表,國內網絡可能較難訪問,需自行解決)