從圖中能夠看出,每門課所需的時間都是固定一小時。具體到這個模式的應用,由於其原理、結構和實現起來都至關簡單,本文不經過示例詳細講解了。可參考示例包中的Course timetabling中的設計和代碼。html
從上圖能夠看到,每一個會議所需的時間長度是不相等的,可是其長度必然是一個Time Grain的倍數,從圖中上方的時間刻度能夠比劃出一個TimeGrain應該是15分鐘。例如Sales meeting佔用了4個Time Grain,即時長1小時。Time Grain模式的使用會相對Time Slot更靈活,適用範圍會更廣。經過設置可知,其實適用於Time Slot模型的情形,是徹底能夠經過TimeGrain模式實現的,只是實現起來會更復雜一些。那麼Time Grain模式的設計要點在哪裏呢?要了解其設計原理,就得先掌握Time Grain的結構及其對時間的提供方法。微信
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模式與其兩種時間規劃模式不一樣,本質上它並不對時間進行規劃,只對實體之間的關係進行規劃優化。所以,在引擎每個原子操做中須要經過對VariableListener接口的實現,來對時間進行推算,並在完成推算後,由引擎經過評分機制進行約束評分。一個Move有可能對應多個原子操做,一個Move的操做種類,能夠參見開發 手冊中關於Move Selector一章,在之後對引擎行爲進行深刻分析的文章中,我將會寫一篇關於Move Seletor的文件,來揭示引擎的運行原理。在須要進行時間推算時,能夠經過實現接口的afterVariableChanged方法,對當前所處理的規劃實體的時間進行更新。由於Chained Through Timea模式下,全部已初始化的規劃實體都處在一條鏈上;所以,當一個規劃實體的時間被更新後,跟隨着它的後一個規劃實體的時間也須要被更新,如此類推,直到鏈上最後一個實體,或出現一個時間正好不須要更新的規劃實體,即該規劃實體前面的全部實體的時間出現更新後,其時間不用變化,那麼鏈上從它日後的規劃實體的時候也無需更新。網絡
如下是VariableListener接口的afterVariableChanged及其處理方法。ide
// 實現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模式下有兩種做用,分別是:ui
1. 用於創建兩個對象(Entity或Anchor)之間的雙向依賴關係;即示例中的Machine與Task, 相鄰的兩個Task。google
2. 用於指定當genuine planning variable的值在規劃運算過程產生變化時,須要更改哪一個變量;即上面提到的開始時間。spa
,對於第一個做用,其代碼體現以下,在規劃實體(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的應用,請發電郵致:kentbill@gmail.com
或到討論組發表你的意見:https://groups.google.com/forum/#!forum/optaplanner-cn
如有須要可添加本人微信(13631823503)或QQ(12977379)實時溝通,但因本人平常工做繁忙,經過微信,QQ等工具可能沒法深刻溝通,較複雜的問題,建議以郵件或討論組方式提出。(討論組屬於google郵件列表,國內網絡可能較難訪問,需自行解決)