开篇
在前面一篇关于规划引擎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>
此文经历了超过两个星期的时间才完成,其实思路与目标在行文前均已很明确,耐何近段时间确定太忙无法抽身。还请各位见谅。接下来,该系列文章将按两个方案开展,一方面按Optaplanner的各个特性,详细讲解各种功能的使用方法与工作原理。另一方面将会类似于本文,将撰写数篇相对深入的应用文章,分享给对Optaplanner有一定认识的同学。如果对此,大家有何建议,欢迎大家加我企鹅一起探讨:12977379或V信:13631823503。
其实 Optaplanner不需要对Java过份精通即可使用,因为它使用到的都是Java最基本的知道,但还是需要有基本的Java知识才行,希望大家找我研究讨论时,如果Java, Maven等方面仍接触较少,请大家先行自补该方面的知识,本猿暂时只能跟大家探讨Optaplanner, Drools的应用,而Java相关的知识,恕无法提供有效的帮助,毕竟本猿也只是个Java新手。先谢了。
若对此文(或本系列任何内容)感兴趣,欢迎转载,但请尊重艰辛劳动,注明出处。为谢!
另外帮Optaplanner老大Geoffrey推广一下,望大家多多关注Optaplanner - http://www.optaplanner.org
一个IT老农,先尽力好当儿子、丈夫和父亲的责任,然后做点有趣的事。