開篇
在前面一篇關於規划引擎Optapalnner的文章里(Optaplanner規划引擎的工作原理及簡單示例(1)),老農介紹了應用Optaplanner過程中需要掌握的一些基本概念,這些概念有且於后面的內容的理解,特別是關於將約束應用於業務規則上的理解。承上一文,在本篇中將會減一些理論,而是偏向於實踐,但過程中,借助實際的場景對一些相關的理論作一些更細致的說明,也是必要的。本文將會假設我們需要對一個車間,需要制定生產計划.我們為生產計划員們設計一套智能的、自動的計划系統;並通過Optaplanner把這個自動計划系統開發出來。當然,里面的業務都是經過高度抽象形成的,去除了復雜的業務規則,僅保留可以體現規划引擎作用的一些業務需求。因此,這次我們只用一個簡單的小程序即可以演繹一個自動計划系統,來呈現規划引擎Optaplanner在自動計划上的魅力。
“項目”背景與業務規則的分類
假如我們接到一個項目,經過需求調研之后,發現其業務邏輯非常簡單;但細想一下業務操作卻又是異常復雜(先別砸磚,聽老農繆繆道來)。它是一個生產計划系統(應該說是一個生產計划輔助系統,畢竟最終的計划,應該是人來決定,而非系統),在沒有這個系統之前,計划人員(生產調試員)每天收到需要加工的生產任務之后,根據當時的機台產能情況,將這些待處理的任務合理地分配到適合的機台。對於前面這句對計划制定工作的描述,其實可以細作提練,其隱含了兩個意義,分別是“合理地”和分配到“合適的”機台。對於這兩個意義,我們可以把它區分為兩種個方面的業務要求:
- “合適的機台” - 表示確定性的條件判斷,是一個定性的斷言,也就是非對即錯。
- “合理地” - 表示非確定性的條件,也就是定量的,可以是非常合理,60%的合理,或完全合理,也就是說是否合理,還是有議論空間的,並沒有一個完全固定的標准。
下面將對上述兩項進行更深入的討論。
確定性條件(定性)
對於上述提到的這兩個條件,其中“分配到合理機台”是相對確定的命題,只要向計划人員提供合理機台的條件指引,計划人員根據這些條件進行操作,總有一天能把所有的任務,匹配上所有條件的,只是時間問題。例如:A類任務只能放在可以處理A類任務的機台上加工;但也可能會更復雜,例如:來自某些客戶的、且具有特定工藝要求的、且生產量在指定范圍內的, 且...,且.....,你可以一直"且"下去,令這些條件非常復雜。但無論怎么復雜,這些條件是具有確定性的判斷標准,也就是說,不管有多復雜,只要能識別出來的條件,生產計划人員就可以根據這些條件進行分配;當然實際生產活動中,必然會遇到一些問題,當一些任務與機台的匹配條件非常復雜時:一來會令工作效率驟降;再就是人是有可能出錯的,比較容易出問題的;甚至超出人的處理能力。
在本文,我們僅僅是為了讓程序可以體現這種確定性條件的處理方法,我們把這類條件簡化到最極端的情況:只有一個條件,只要機台可處理的任務類型,與任務自己的類型合適即表示機台與任務匹配。例如:有個機台M1可以做的T1, T2,這兩種任務,機台2可以做T2,T3兩種任務;那么,如果一個任務它是屬於T1類型,則合適的機台只有M1, 如果這個任務是T3類型,則它的合適機台只有M2;如果這個任務是T2類型,則合適的機台有M1,M3兩台。凡是把任務分配相應類別的機台上去,都是合適的。
非確定性條件(定量)
非確定性條件,相對復雜一點,因為這類條件是沒有絕對的對錯,只有相對的優劣。例如本文中我們會使用成本這個條件因素,在確保上面的確定性因素(任務類型與機台匹配)前提下,成本越低越好。那么就存在多個可行方案的的可能,就會涉及到應該如何計算成本,用哪些方案的成本進行對比才是合理的問題。要理清這些問題,需要對業務模型有深入的理解,並能很准確地對這些因素進行歸納,把它們抽象成約束條件。為了簡化問題,在本例中,成本反映在機台的啟用成本上。也就是說,每個機台一旦啟動它都會產生固定成本,而不會隨着任務量增多而成本上升。所以作為計划定制人員,如果這是一個計划的重要指標的話,在制定計划時,就需要考慮應該如何統計一個機台的成本。本例中我們假定某個機台一旦啟動,即產生固定的成本,所以我們的目標是:
- 用盡量小的機台來完成盡量多的任務.
- 這些被啟用的機台,其成本盡可能低;即優先使用低成本的機台來處理任務。
當然,因應不同的場景,會有其它的要求,例如產能平衡:令盡可能多的機台被啟用,以減少少空置率,提高生產效率。本例中我們並沒有使用此規則。
設計與測試數據
為了滿足上述條件,我們先建立業務模型。我們先識別出業務實體。可以識別出來的實體也只有兩個,機台和任務。
機台
我們假設有6個機台,分別是M1- M6, 它們分別有自己可處理的任務類型:Type_A,Type_B, Type_C 和 Type_D, 且分別有自己的產能和成本。產能表示這個機台在固定時間段內,最多可以處理的任務量;成本表示如果這個機台一旦開啟,即產生相應的貨幣成本。例如:機台M1,
- 它可以處理類型為Type_A的任務(也就是說,它可以生產類型為Type_A的產品);
- 在固定時間段內(例如一個班次,或一天)可以處理300個任務,即產能為300。
- 它的成本為100, 即它一旦被啟動,即產生100元的成本.
所有機台資料如下圖,可以看到,有些機台它的可處理的任務類型是相同的,但兩者的產能不同;有些可處理的任務類型相同,產能也相同,但成本不同;這樣就進一步貼近實況。

任務(產品)
對於需要加工的產品(工稱工件),我們把它抽象成任務,因為對於一個車間中的機台而言,以任務來識別它更貼切一些,在實際的業務建模中,一個產品不一定是一個任務,也有可能是一個產品的工序路線中的其中一個工序被定義為一個任務,即表示一個生產單位的一個生產指令, 例如:對機器外殼打孔。而在本例中,我們了為簡化問題,我們假設一個任務就一個產品,每個產品只需一個任務即可。而關於一個產品存在一條完整且復雜的工序路線,從而產生多個生產任務的情況,我將在以后的文章中,關於Optaplanner的更高級的應用中,將會有相關的詳細講解。
對於任務(產品),我們的假設它具有類型和生產量兩個屬性。類型-表示它是屬於哪一類的產品,用於識別它可以被分配到哪一個機台進行加工處理。生產量-表示這個產品需要生產多少個,當這個產品被分配到指定的機台上生產的時候,生產量這個屬性將會與對應機台的產能作出對比與限制,即一個任務如果生產量超過了一個機台的產能,那么這個任務就無法放在這個機台上處理。所有任務(10個)的資料如下圖:

約束
假如我們已經通過需求調研,確定了我們上述機台與任務兩個業務實體,那么,下一步的調研目標,就是要識別出在這些任務分配到機台上的過程中,按照生產業務要求,我們需要遵循哪些規則了。本例我們假設有以下業務規則,以下稱為約束,其中包括硬約束(不可違反),和軟約束(盡量不要違反,但將不可避免;如果違反,盡可能令違反的程度減到最小)
硬約束:
- 任務只能被分配到可以處理它的機台上,以機台的“可處理任務類型”字段與任務的“類型”字段作識別,兩者一致才符合條件;
- 一個機台處理的任務的生產量總和不能超過其產能。
軟約束:
- 整個排產計划中,所有啟用的機台成本之和盡量小。
通過上述約束的描述,可以得知,其中兩個硬約束是可以避免的,但軟約束是不可避免的,因為你處理任務必須啟動機台,一旦啟動任意機台,都會產生成本。因此,軟約束的要求是盡量小,而不是不違反,不是0.
任務分本問題的解決方法(暴力窮舉法與Optaplanner)
以下為理論部分,無興趣探討的同學可以跳過本小節。
本“項目”的業務場景、業務實體和業務規則,我們都已經構建完成,接下來就是如何在上述給定條件的基礎上,構建一個快速可用的解決方案,用於解決任務的分配問題了。至此,可能有些同學在想,其實這並不難呀,根據給定的兩個硬約束和一個軟件約束,以兩個硬約束作為限制條件,通過暴力窮舉的方法,找出一個無限趨近於符合軟約束,也可以找出一個令成本最低的任務分配方案出來呀。這是對的,只要我們有明確的軟硬約束要求,理論上是可以寫出對應的程序,通過強大的CPU算法,甚至可以將程序寫成並發運算,集成數量龐大的GPU算力,興許能找最終方案的。但是,有這種想法,其實忽略了問題的規模與時間復雜度的關系。我們需要探討,隨着數據量(即問題規模)的增大,找到可用分配方案的耗時增長有多大,與問題規模的增長呈何種比例關系。通過上述的條件,及排列組合的知識得知,通常這類問題的時間復雜度是指數級復雜度,即O(a^n),甚至是階乘級復雜度的,即O(n!)。當數據量有限增大之后,所需的運行時間增長,對目前技術上的計算機算力來講,增長是指數級,甚至以今天的技術水平,是永遠都無法找到最終方案的。這個在關於NPC或NP-Hard問題的文章中已有介紹,這里不再重復。
面對這類NP問題時,人類是如何解決的呢。其實人類目前也是無解的,如果哪位同學如果找到一個算法來解決這類問題,它的時間復雜度是常數級的,那么恭喜你,你已經為人類解決了不少難題了。而所謂的無解是指,無法在任何情況下找出一個絕對最優的解決方案(如果本例中的業務規則及數據量,用草稿紙都可以把所有情況列出來了,當然可以找出最優解,前提是你有足夠耐性).所以,人們想到的還是通過窮舉的方法,一個一個的組合方案去嘗試,直找到最佳方案。但這種方法在數據量增大,或更多判斷條件的時候是不可行的,而我們日常處理這類問題(例如排生產計划),當找到的排產方案只要滿足了所有硬約束(其實光是滿足硬約束的方案,如果不通過程序來實現,人類也很難很快找到);軟約束方面只要找出一個差不多的,我們即可視作一個可用的方案並付諸執行了;因為我們不可能無限地找下去。而Optaplanner其實跟我們一樣,問題規模足夠大的情況下,它也是不可能找出絕對最優方案的。但是它相對人類聰明之處在於,它集成了尋找最優方案的過程諸多專門的算法。過程中使用了分階段,分步驟的方法將問題歸約成一些數學模型可處理的形式。且在尋找最佳方案(應該是尋找更佳方案)的過程中,它集成了一堆已被證明卓有成效的數學尋優算法,例如在問題初始化階段可以使用First Fit, First Fit Decreasing等算法,在尋優階段使用禁忌搜索法、模擬退火法等算法。從而極大地提高尋找更優方案的效率。令其在相同的時間內,找到的方案相對人類,或者相對不使用任何算法的暴力窮舉法而言,質量高得多,更趨近於最優方案。
用Optaplanner解決任務分配問題
通過Optaplanner尋找更佳分配方案,需要建立相關的類和模型,英語還可以的同學,可以直接上去它的使用說明中查看Cloud Balance示例,是一個非常好的示例,從最簡單的Hellow world, 到使用了Real-time planning等近幾個版本新功能,都有詳細的說明與教程。我們現在這個示例也是參照它來設計的。
在開始設計之前,我們需要構思一下,我們的任務分配是如何實現的。在我們的實際計划制定的業務操作中,也就是工廠的車間里,計划員是把一個產品的實物,喂進一個機台,讓機台對它進行處理。所以我們會理解為:分配就是把上面10個任務分配到6個機台中。其實這樣做是可行的,但我們更深入地思考一下,其實我們需要處理的是任務,而不是機台,也就是說,每個任務必須都被分配到一個機台中處理,而機台不一定。在最小化成本的原則底下,好的方案有可能出現有部分機台並沒有分得任務的情況。所以,我們需要把任務與機台的關系倒過來,把任務作為我們的研究目標,理解為把一個機台分給一個任務。做過生產計划或生產管理的同學就很清楚知道,機台也是一種生產資源,針對不同的生產任務,將資源應用到這些任務上去,其中機台(或產線)是一個很常見的資源類型。
所以,我們的設計思路是:針對一個任務,把一個適合的機台分配給它,令它滿足兩個條件:1. 滿足所有硬約束; 2.分配好所有任務的機台之后,這些機台的成本加總盡量低。好了,理清了思路,下面我們就可以開始設計了。
按Optaplanner規范建模
要使用Optaplanner規划引擎,就需要按它的要求建立對應的模型,包括各種類及其關系。我們這個示例跟官網上的Cloud Balance幾乎一致,在它的類圖基礎上修改就可以了。我們先看看建立好的class diagrame如下圖:

要建立的類分別是:
- Task,表示任務的實體類,它被注解為@PlanningEntity, 它有三個屬性:taskType - 當前任務的類型; quantity - 生產量;machine - 任務將要被分配到的機台。其中machine屬性被注解為@PlanningVariable, 表示規划過程中,這個屬性的值將被plan的,即通過調整這個屬性來得到不同的方案。另外,作為一個Planning Entity, 它必須有一個ID屬性,用於在規划運行過程中識別不同的對象,這個ID屬性被注解為@PlanningId。 本例中所有實體類都繼承了一個通用的類 - AbstractPersistable, 該父類負責維護此所有對象的ID。Task類也繼承於它,因此,將該類的ID屬性注解為@PlanningId即可。另外,作為Planning Entity, 它必須有一無參構造函數,若你在此類實現了有參構造的話,需要顯式地實現一個無參構造函數。
- Machine, 表示機台的實體類,它屬於ProblemFact,在其中保存了在規划過程中會用到的屬性,此類反映一個機台相關信息的屬性: taskType - 可處理的任務類型; capacity - 當前機台的產能; cost -當前機台的成本。
- TaskAssignment, 此類用來描述整個解決方案的固定類,它的結構描述了問題的各種信息,在Optaplanner術語中,在執行規划前,它的對象被稱作一個Problem, 完成規划並獲得輸出之后,輸出的TaskAssignment對象被稱作一個Solution。它具有固定的特性要求: 必須被注解為@PlanningSolution;本例中,它至少有三個屬性: machineList - 機台列表,就是可以用於分配任務的機台,本例中指的就是上述那6個機台;taskList - 就是需要被規划(或稱分配機台)的10個任務,這個屬性需要被注解為@PlanningEntityCollectionPropery. 還有一個是score屬性,它用於在規划過程中對各種約束的違反情況進行打分,因為本例中存在了硬約束與軟約束。因此我們使用的Score為 HardSoftScore.
另外,上述提到了一個的有實體類(本例只有Task與Machine為實體類)的父類AbstractPersistable, 它負責維護ID屬性,對實體類的compareTo方法,toString方法進行重載。
具體的代碼如下,沒有Maven基礎的同學,請先自補一下Maven的知識,以下內容都是基於Maven的。
Task類:

1 package com.apsbyoptaplanner.domain; 2 3 import org.optaplanner.core.api.domain.entity.PlanningEntity; 4 import org.optaplanner.core.api.domain.variable.PlanningVariable; 5 6 @PlanningEntity 7 public class Task extends AbstractPersistable{ 8 9 private String requiredYarnType; 10 private int amount; 11 12 private Machine machine; 13 14 public String getRequiredYarnType() { 15 return requiredYarnType; 16 } 17 18 public void setRequiredYarnType(String requiredYarnType) { 19 this.requiredYarnType = requiredYarnType; 20 } 21 22 public int getAmount() { 23 return amount; 24 } 25 26 public void setAmount(int amount) { 27 this.amount = amount; 28 } 29 30 @PlanningVariable(valueRangeProviderRefs={"machineRange"}) 31 public Machine getMachine() { 32 return machine; 33 } 34 35 public void setMachine(Machine machine) { 36 this.machine = machine; 37 } 38 39 public Task(){} 40 41 public Task(int id, String requiredYarnType, int amount) { 42 super(id); 43 this.requiredYarnType = requiredYarnType; 44 this.amount = amount; 45 } 46 }
Machine類:

1 package com.apsbyoptaplanner.domain; 2 3 public class Machine extends AbstractPersistable{ 4 private String yarnType; 5 private int capacity; 6 private int cost; 7 8 public String getYarnType() { 9 return yarnType; 10 } 11 public void setYarnType(String yarnType) { 12 this.yarnType = yarnType; 13 } 14 15 public int getCapacity() { 16 return capacity; 17 } 18 public void setCapacity(int capacity) { 19 this.capacity = capacity; 20 } 21 public int getCost() { 22 return cost; 23 } 24 public void setCost(int cost) { 25 this.cost = cost; 26 } 27 public Machine(int id, String yarnType, int capacity, int cost) { 28 super(id); 29 this.yarnType = yarnType; 30 this.capacity = capacity; 31 this.cost = cost; 32 } 33 }
TaskAssignment類

package com.apsbyoptaplanner.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.drools.ProblemFactCollectionProperty; import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider; import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; @PlanningSolution public class TaskAssignment extends AbstractPersistable{ private HardSoftScore score; private List<Machine> machineList; private List<Task> taskList; @PlanningScore public HardSoftScore getScore() { return score; } public void setScore(HardSoftScore score) { this.score = score; } @ProblemFactCollectionProperty @ValueRangeProvider(id = "machineRange") public List<Machine> getMachineList() { return machineList; } public void setMachineList(List<Machine> machineList) { this.machineList = machineList; } @PlanningEntityCollectionProperty @ValueRangeProvider(id = "taskRange") public List<Task> getTaskList() { return taskList; } public void setTaskList(List<Task> taskList) { this.taskList = taskList; } public TaskAssignment(List<Machine> machineList, List<Task> taskList) { //super(0); this.machineList = machineList; this.taskList = taskList; } public TaskAssignment(){} }
AbstractPersistable類

1 package com.apsbyoptaplanner.domain; 2 3 import java.io.Serializable; 4 5 import org.apache.commons.lang3.builder.CompareToBuilder; 6 import org.optaplanner.core.api.domain.lookup.PlanningId; 7 8 public class AbstractPersistable implements Serializable, Comparable<AbstractPersistable> { 9 10 protected Long id; 11 12 protected AbstractPersistable() { 13 } 14 15 protected AbstractPersistable(long id) { 16 this.id = id; 17 } 18 19 @PlanningId 20 public Long getId() { 21 return id; 22 } 23 24 public void setId(Long id) { 25 this.id = id; 26 } 27 28 @Override 29 public int compareTo(AbstractPersistable other) { 30 return new CompareToBuilder().append(getClass().getName(), other.getClass().getName()).append(id, other.id) 31 .toComparison(); 32 } 33 34 @Override 35 public String toString() { 36 return getClass().getName().replaceAll(".*\\.", "") + "-" + id; 37 } 38 }
到目前為止,我們已完成了所有的Java代碼了,注意,這里指的是Java代碼,事實上要成功啟動Optaplanner的規划引擎,只有Java代碼是遠遠不夠的。還需要更多的配置與其它內容。
對於上述代碼,眼尖的同學應該會看到,在TaskAssignment類中,machineList的getter - getMachineList(),除了被注解為@ProblemFactCollectionProperty, 還有另外一個注解 @ValueRangeProvider(id="machineRange"), 滿腦疑惑的同學先不急,大家再看看Task類的machine成員的getter - getMachine()。奇怪了上文不是提到,它只需被注解為@PlanningVariable的嗎?怎么后面還有個參數呢,整個注解是@PlanningEntity(valueRangeProviderRefs={"machineRange"}), 沒錯了,大家應該猜到,這兩個注解的意義了。它們的意義是:對於每個Planning Entity (task) 對象中的PlanningVariable(machine),它的取值范圍是TaskAssignment對象中的machineList中的內容。從業務上講,就是說,對於每一個任務而言,它可以分配的機台,是那6個機台之一。這樣大家是否恍然大悟呢?
好了,上面已經巧妙地通過各個注解,將Planning Entity, Problem Fact和Problem等對象關聯起來,那么大家是不是覺得有些地方漏了?對了,那就是約束規則(2硬1軟的約束)如何在這些類的關系中體現呢?其實上面這些類關系是沒辦法表達這些業務約束的;如果需要表達這些約束,還需要創建一些用於計分數的類,用於對每個約束的違反情況進行記分。但自從Optaplanner與Drools(一個開源規則引擎)結合之后,就不再需要自己通過Java代碼編寫算分邏輯了(當然你也可以不用Drools,自行編寫算分邏輯),只需要通過Drools表達業務約束,Optaplanner在規划過程中,會啟自行啟動Drools規划引擎對這些約束進行判斷,從而進行計分。
那么我們只需要在resource里添加一個Drools腳本文件,用於描述這些約束即可。至於Drools的應用,不在本文范圍,同學們可以自行學習Drools,如有需要,我將會撰寫另外一個Drools應用相關的系列文章 .
rules.drl文件
1 package com.apsbyoptaplanner.solver; 2 3 import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScoreHolder; 4 import com.apsbyoptaplanner.domain.Task; 5 import com.apsbyoptaplanner.domain.Machine; 6 import com.apsbyoptaplanner.domain.TaskAssignment; 7 8 global HardSoftScoreHolder scoreHolder; 9 10 rule "yarnTypeMatch" 11 when 12 Task(machine != null, machine.yarnType != requiredYarnType) 13 then 14 scoreHolder.addHardConstraintMatch(kcontext, -10000); 15 end 16 17 rule "machineCapacity" 18 when 19 $machine : Machine($capacity : capacity) 20 accumulate( 21 Task( 22 machine == $machine, 23 $amount : amount); 24 $amountTotal : sum($amount); 25 $amountTotal > $capacity 26 ) 27 then 28 scoreHolder.addHardConstraintMatch(kcontext, $capacity - $amountTotal); 29 end 30 31 rule "machineCost_used" 32 when 33 $machine : Machine($cost : cost) 34 exists Task(machine == $machine) 35 then 36 scoreHolder.addSoftConstraintMatch(kcontext, -$cost); 37 end
好了,實體模型我們創建好了,約束也已通過Drools腳本表達出來了,Optapalnner是如何將兩者結合起來,從而達到計分效果的呢?其實我們還是缺了一塊,那就是Optaplanner的配置,因為需要創建Optaplanner的引擎對象進行規划的時候,是有一大堆參數需要指定給引擎的。按照Optaplanner的接口設計要求,需要設計一個稱作Solvder Configuration的XML文件,用於描述引擎的參數及行為。
taskassignmentConfiguration.xml:
<?xml version="1.0" encoding="UTF-8"?> <solver> <!-- Domain model configuration --> <solutionClass>com.apsbyoptaplanner.domain.TaskAssignment</solutionClass> <entityClass>com.apsbyoptaplanner.domain.Task</entityClass> <!-- Score configuration --> <scoreDirectorFactory> <scoreDrl>taskAssignmentDools.drl</scoreDrl> </scoreDirectorFactory> <!-- Optimization algorithms configuration --> <termination> <secondsSpentLimit>10</secondsSpentLimit> </termination> </solver>
好了,通過上述的步驟,一個Optaplanner程序基本上就完成了。且慢!一個Java程序竟然沒有main入口?沒錯,除了main入口外,我們還沒有構建引擎對象並啟動它呢。因為是示例,我就將構造引擎對象,業務實體對象都放在入口程序App里。
App.java代碼
1 import java.io.InputStream; 2 import java.util.ArrayList; 3 import java.util.List; 4 import java.util.stream.Collectors; 5 import org.optaplanner.core.api.solver.Solver; 6 import org.optaplanner.core.api.solver.SolverFactory; 7 import com.apsbyoptaplanner.domain.Machine; 8 import com.apsbyoptaplanner.domain.Task; 9 import com.apsbyoptaplanner.domain.TaskAssignment; 10 11 public class App { 12 13 public static void main(String[] args) { 14 startPlan(); 15 } 16 17 private static void startPlan(){ 18 List<Machine> machines = getMachines(); 19 List<Task> tasks = getTasks(); 20 21 InputStream ins = App.class.getResourceAsStream("/taskassignmentConfiguration.xml"); 22 23 SolverFactory<TaskAssignment> solverFactory = SolverFactory.createFromXmlInputStream(ins); 24 Solver<TaskAssignment> solver = solverFactory.buildSolver(); 25 TaskAssignment unassignment = new TaskAssignment(machines, tasks); 26 27 TaskAssignment assigned = solver.solve(unassignment);//啟動引擎 28 29 List<Machine> machinesAssigned = assigned.getTaskList().stream().map(Task::getMachine).distinct().collect(Collectors.toList()); 30 for(Machine machine : machinesAssigned) { 31 System.out.print("\n" + machine + ":"); 32 List<Task> tasksInMachine = assigned.getTaskList().stream().filter(x -> x.getMachine().equals(machine)).collect(Collectors.toList()); 33 for(Task task : tasksInMachine) { 34 System.out.print("->" + task); 35 } 36 } 37 } 38 39 40 private static List<Machine> getMachines() { 41 // 六個機台 42 Machine m1 = new Machine(1, "Type_A", 300, 100); 43 Machine m2 = new Machine(2, "Type_A", 1000, 100); 44 Machine m3 = new Machine(3, "TYPE_B", 1000, 300); 45 Machine m4 = new Machine(4, "TYPE_B", 1000, 100); 46 Machine m5 = new Machine(5, "Type_C", 1100, 100); 47 Machine m6 = new Machine(6, "Type_D", 900, 100); 48 49 List<Machine> machines = new ArrayList<Machine>(); 50 machines.add(m1); 51 machines.add(m2); 52 machines.add(m3); 53 machines.add(m4); 54 machines.add(m5); 55 machines.add(m6); 56 57 return machines; 58 } 59 60 private static List<Task> getTasks(){ 61 // 10個任務 62 Task t1 = new Task(1, "Type_A", 100); 63 Task t2 = new Task(2, "Type_A", 100); 64 Task t3 = new Task(3, "Type_A", 100); 65 Task t4 = new Task(4, "Type_A", 100); 66 Task t5 = new Task(5, "TYPE_B", 800); 67 Task t6 = new Task(6, "TYPE_B", 500); 68 Task t7 = new Task(7, "Type_C", 800); 69 Task t8 = new Task(8, "Type_C", 300); 70 Task t9 = new Task(9, "Type_D", 400); 71 Task t10 = new Task(10, "Type_D", 500); 72 73 List<Task> tasks = new ArrayList<Task>(); 74 tasks.add(t1); 75 tasks.add(t2); 76 tasks.add(t3); 77 tasks.add(t4); 78 tasks.add(t5); 79 tasks.add(t6); 80 tasks.add(t7); 81 tasks.add(t8); 82 tasks.add(t9); 83 tasks.add(t10); 84 85 return tasks; 86 } 87 }
好了,上述代碼懂的同學應該不難理解,就是先構建一個Solver對象,一個taskList, 一個machineList;及一個taskAssignment對象,並taskAssignment對象對應的成員分別指向taskList與machineList. 然后就啟動solver對象的solver方法。引擎就哄哄地被啟動,去幫我們找最優解了。
如果配置好log組件,大家將會看到如下輸出:
22:20:25.687 [main] DEBUG LS step (26556), time spent (9999), score (0hard/-800soft), best score (0hard/-700soft), accepted/selected move count (1/5), picked move (Task-1 {Machine-2} <-> Task-4 {Machine-1}). 22:20:25.687 [main] DEBUG LS step (26557), time spent (9999), score (0hard/-800soft), best score (0hard/-700soft), accepted/selected move count (1/1), picked move (Task-3 {Machine-2 -> Machine-1}). 22:20:25.688 [main] DEBUG LS step (26558), time spent (10000), score (0hard/-800soft), best score (0hard/-700soft), accepted/selected move count (1/22), picked move (Task-3 {Machine-1} <-> Task-4 {Machine-2}). 22:20:25.688 [main] INFO Local Search phase (1) ended: time spent (10000), best score (0hard/-700soft), score calculation speed (31205/sec), step total (26559). 22:20:25.689 [main] INFO Solving ended: time spent (10001), best score (0hard/-700soft), score calculation speed (30447/sec), phase total (2), environment mode (REPRODUCIBLE). Machine-2:->Task-1->Task-2->Task-3->Task-4 Machine-4:->Task-5 Machine-3:->Task-6 Machine-5:->Task-7->Task-8 Machine-6:->Task-9->Task-10
從上面的日志內容,我們可以看到,以時間開始的行,是Optaplanner引擎在一步一步幫我們找最優方案時的過程輸出。到最后結束時,它顯示 best score (0hard/-700soft)。 意思是說,它幫我們找到的方案的評價是:沒有違反任何硬約束(0hard), 軟約束的違反分數是700分(-700soft). 也就是我們用這些機台做完這10個任務需要700元的機台成本.那么這700元是怎么來的呢?那就得看看它給出我們的分配方案是什么了:
Machine-2:->Task-1->Task-2->Task-3->Task-4 Machine-4:->Task-5 Machine-3:->Task-6 Machine-5:->Task-7->Task-8 Machine-6:->Task-9->Task-10
意思是說 M2(Machine-2)上分配了Task1, Task2, Task3, Task4; 其它機台如此類推。即應用了M2,M3,M4,M5,M6共5個台機,大家可以回到上面的機台列表,這5個機台的成本加起來就是700元。
至此,Optaplanner已經幫大家找到最佳方案了,大家可以自行驗證一下,試試如何將上面分配方案的一些任務移到其它機台,它能否保持不違反2個硬約束的前提下,得到比700更小的機台成本?
另外,關於Maven需要的依賴包,我將POM文件的內容也貼出來。大家照着上,應該可以運行起來了。
POM.XML文件內容:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.optaplanner</groupId> <artifactId>optaplanner</artifactId> <version>7.8.0.Final</version> </parent> <groupId>com.apsbyoptaplanner</groupId> <artifactId>OptaplannerTest</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-core</artifactId> </dependency> <dependency> <groupId>org.kie</groupId> <artifactId>kie-api</artifactId> </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <scope>runtime</scope> </dependency> </dependencies> </project>
本系列文章在公眾號不定時連載,請關注公眾號(讓APS成為可能)及時接收,二維碼:
如需了解更多關於Optaplanner的應用,請發電郵致:kentbill@gmail.com
或到討論組發表你的意見:https://groups.google.com/forum/#!forum/optaplanner-cn
若有需要可添加本人微信(13631823503)或QQ(12977379)實時溝通,但因本人日常工作繁忙,通過微信,QQ等工具可能無法深入溝通,較復雜的問題,建議以郵件或討論組方式提出。(討論組屬於google郵件列表,國內網絡可能較難訪問,需自行解決)