时间规划在Optaplanner上的实现

  在与诸位交流中,使用较多的生产计划和路线规划场景中,你们最为关注的焦点是关于时间的处理问题。确实,时间这一维度具备必定的特殊性。由于时间是一维的,体现为经过图形表示时,它仅能够经过一条有向直线来表达它的时刻和方向。相对而言,空间则能够存在多维,例如二维坐标,三维空间等,甚至在生产计划的规划场景中,各类资源能够表示为多个维度。所以,时间的一维特性,决定了在规划过程当中,须要处理它的方法也具备必定的特殊性和局限性。本文将讨论经过Optaplanner实现规划过程当中,对于时间方面的处理方式。
在众多规划优化场景中,能够概括为两种状况的规划,分别是单一维的空间维度规划,和同时存在空间与时间两个维度进行规划。
其中第一种状况,仅对一个维度进行规划的场景,咱们能够把这一维概括为,仅对空间维度的规划。例如八王后(N Qeen)问题,其规划的目标是为每一个王后找个适当的位置,位置就是一个最为直观的空间概念,所以它是一个很明确直观的空间规划问题。而另一些从直接字面意义上可能跟空间并无直接的关系,但其实也能够将它视做仅有一个空间维度的规划。这类规划的一个特色规划目标与目标之间没有时序关系,即时间维度是不考虑的,例如。有一些存在时间概念的问题,其实也能够转化为惟一空间维度的规划,从而将问题简化。例如排班过程当中,将每一个人员安排到指定的班次,虽然班次是一个时间上概念的概念,但实际对这个问题进行排班设计的时候,咱们能够将时间转化为相似空间的形式处理。更直观的说法,将班次分布在时间轴上,按时间轴来看,各个班次就是时间轴上不一样位置的区间,从而令问题简化。所以,这类规则更严格地说,能够理解为不管是空间仍是时间上的规划,均可以转化、展开为单一惟度的规划问题,经过使用空间规划的方法进行规划建模求解;即便是时间规划(例如排班)也不例外。
  另一种规划,则须要同时考虑空间与时间两个维度协同规划。如生产计划、带时间窗口的车辆路线规划等问题,就是其中的典型。以生产计划为例,在空间维度,须要将一个任务分配到合理的机台,便是空间上的规划。然而,生产计划问题的另外一个需求是,肯定了机台后,还要肯定到底这个任务应该在何时开始,何时结束;哪一个任务须要在哪一个任何完成后才能开始等等。这些时序逻辑相关的引出的问题,均属于时间规划问题。时间维度能够与空间维度一块儿,肯定一个活动的时空坐标。此坐标是一个逻辑上抽象的概念。以生产计划为例,两个维度均经过平面图形来表示时,能够把计划中的每一个任务,分配在指定机台的指定时间区间上,经过下图能够看到,这个示意图的水平轴(X轴)表示时间,从这个方向能够看出一个任务哪一个时刻开始,持续多久,哪一个时刻结束。以及与该任务同处于一个空间(机台,或产线,或车间)上的先后任务的接续关系。垂直轴(Y轴)表示空间,表示它被分配到哪一个机台上执行。以下图:
  针对不一样的时间规划要求,Optaplanner提供了3经常使用的规划模式,分别是时间槽模式- Time Slot Pattern,时间粒模式 - Time Grain Pattern, 和时间链模式 - Chained Through Time Pattern.下面分别对这三种模式的特征,适用场景和使用方法进行详细介绍。由于翻译准确度缘由(对本身的英文水平缺少自信:P), 下文介绍中均直接使用Time Slot, Time Grain 和 Chained Through Time.以免本文件的翻译不当形成误解。
 

时间槽模式 - Time slot

Time Slot在应用时有一些适用条件,知足如下全部条件,才适用:
  1. 规划实体中的规划变量是一个时间区间;
  2. 一个规划变量的取值最多仅可分配一个时间区间;
  3. 规划变量对应的时间区间是等长的。
 
  对于规划值范围各个时间段,将其转换为空间上的概念更为直观。将时间用一个水平轴表示,在轴上划分大小固定的区间,这些区间则能够做为规划过程当中的取值范围;在设计时,把这些区间定义成ValueRange。适用于Time slot模式状况,有制定中小学课程表、考试安排等问题。由于大学或公开课程的计划安排,除了排定时间外,可能还须要肯定具体的地点,也就是空间维度的规划。此类问题一般须要将时间和空间分开来考虑,但其中的时间纬能够经过Time slot模式转化为与空间规划同样的问题,从而令问题简化。引用Optaplanner开发手册的一张图能够清楚地看到,每个规划实体只须要一个时间区间,且区间长短是相同的,(以下图)。
 

    从图中能够看出,每门课所需的时间都是固定一小时。具体到这个模式的应用,由于其原理、结构和实现起来都至关简单,本文不经过示例详细讲解了。可参考示例包中的Course timetabling中的设计和代码。html

 时间粒模式 - Time Grain

  在至关多运筹优化场景中,须要规划的时间长短是不固定的,不一样的任务其所需的时间有长短之分。这种需求下,若使用Time slot模式就没法实现时间上的精确规划。那些就要使用更灵活,时间粒度更小的Time Grain模式。从Time Grain模式的名称中的Grain能够推测到,此模式是将时间细分红一个一个颗粒并应用于规划。例如能够设定为每1分钟,5分钟,30分钟,1小时等固定的长度,为一个Grain的长度。
Time Grain模式适用条件:
  1. 规划变量是时间区间;
  2. 业务上对应于规划变量的时间区间能够不等长,但必须是Grain的倍数。
   例如经过Outlook的日历功能建立会议时,默认状况下每一个会议的时间,是0.5小时的倍数,也就是一个会议至少是0.5小时,或者是1小时,或1.5小时如此类推。固然若是你不使用Outlook的默认时间精度,也能够将时间精度定到分钟,那么也就表示,会议的时间是1分钟的倍数。只不过针对人的平常活动在时间上的精度,以分钟做为精确度其意义不太大。就如9:01分开会跟9:00开会,对于人类的活动能力来讲,正常状况下不存在任何区别。由于你从办公室去到会议室,均可能须要花费1分钟了;因此outlook里默认的是半小时。那么这个最小的时候单位 - 半小时,在Time Grain模式中,就被称为一个Time Grain,如下简称Grain。能够先从开发手册的图中看到Time Grain模式所表达的意义,以下图。
 

   从上图能够看到,每一个会议所需的时间长度是不相等的,可是其长度必然是一个Time Grain的倍数,从图中上方的时间刻度能够比划出一个TimeGrain应该是15分钟。例如Sales meeting占用了4个Time Grain,即时长1小时。Time Grain模式的使用会相对Time Slot更灵活,适用范围会更广。经过设置可知,其实适用于Time Slot模型的情形,是彻底能够经过TimeGrain模式实现的,只是实现起来会更复杂一些。那么Time Grain模式的设计要点在哪里呢?要了解其设计原理,就得先掌握Time Grain的结构及其对时间的提供方法。微信

  Time Grain中的重点在于一个Grain的设计,与Time Slot中的slot同样,Time Grain中的Grain表示的也是一个时间区间,只不过它所表达的意义不只在于一个Time Grain的时间区间内,每一个Grain的序号也是关键因素,当一个Grain被分配到一个规划变量时,Grain的序号决定了它与时间轴的映射位置。在生产计划中,若一个Grain被分配到一个任务时,表示任务起止于这个Grain的开始时刻。 即该任务的开始时间是哪一个Grain内对应的时间区间内,那么这个Grain的开始时间,就是这个任务的开始时间;经过这个任务的长度,推算出它须要占用多少个Grain, 进而推算出它的结束时间会在哪一个Grain内,那么这个Grain的结束时间,便是这个任务的结束时间。
仍是以上图为例,其中的Sales meeting,它的起始是在grain0内,grain0的起始时间是8:00,那么这个会议的起始时间就是8:00。这个会议的长度是1小时,因此它占用了4个Grain,所以,第4个Grain的结束时间就是会议的结束时间,也就是图中Grain3的结束时间 - 9:00,是这个会议的结束时间。进一步分析也知,若这个会议时长是1:10, 那么它的结束时间将会落于gran4内(第5个grain), 那么它的结束时间就是grain4的结束时间 - 9:15. 所以,总结起来,咱们在实现这个模式的时候有如下要点在设计时须要注意:
  1. 设计好每一个Grain的粒度,也就是时间长度。并非粒度越细越好,例如以1秒钟做为一个粒度,是否是就能够将任务的时间精度控制在1级呢?理论上是能够的,但平常使用中不太可行。由于这样的设计会产生过量的Grain,Grain就是Value Range,当可选值的数量过多时,整个规划问题的规模就会增大,其时间复杂度就会指数级上升,从而令优化效果下降。
  2. 定义好每一个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的起始是什么时刻;它决定了后续全部任务的时间。
  为了防止同一空间上,存两个任务时间重叠的问题,能够根据其分配的Grain进行判断。如示例Meeting scheduling中关于时间重叠的判断,能够参考MeetingAssignment类中的calculateOverlap方法,见如下代码。
 
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); }
 
  上述代码是判断两个会议的TIme Grain, 若存在重叠,则返回重叠量,供引擎的评分机制来判断各个solution的优劣。
 

时间链模式 - Chained Through Time

  前面提出的两种时间模式,其实有较多的类似之处,都是将时间段划分为单个个体,再将这些个体做为规划变量的取值范围,从而实现与空间规划一致的规划模式。但更复杂的场景下,将时间转化为“空间”的作法,未必能行得通。例如带时间窗口的路径规划,多工序多资源生产计划等问题,其时间维度是难以经过Time Slot或Time Grain模式实现的。我增尝试将Time Grain模式应用于多工序多资源条件下的生产计划规划;其原理上是可行的,但仍然会到到一些至关难解决的问题。其中之一就是Time Grain的粒度大小问题,若须要实现精确到分钟的计划,当编排一个时间跨度较大的计划时,就会引发问题规模过大的问题,从而论引擎效率骤降。另外就是实现相邻任务的重叠和前后次序判断时,会遇到一些难以解决的,问题须要花费较多的精力去处理。所以,Optaplanner引入了第三种时间规划模式 - 时间链模式(一样是翻译问题,下称Chained Through Time模式)。
Chained Through Time模式顾名思义就是应用了链状结构的特性,来实现时间的规划。它的设计思想是,规划变量并非普通的时间或空间上的值, 而是另一个规划实体;从而造成一个由各个首尾相接的规划实体链,即Value Range的范围就是规划实集合自己。经过规划实体间的链状关系,来推算各个实体的起止时间。事实上,Optaplanner中将规划实体环环相扣造成链的特性,其主要目的并不是为了实现时间规划,而是为了解相似TSP,VRP等问题而提供的。这些问题须要规划的,是各个节点之间造成的连通关系;在约定规则下,求解最佳连通方案。根据不一样的场景要求,所求的目标有“最短路径”,“最小重复节点”,“最在链接效率”等。在时间规划的功能方面,其实现方式与上两种模式相似。以生产计划的例子来讲,经过Chained Through Time模式得到各任务的链接关系与次序后,就能够根据链中首个任务的开始时间,结合各任务的持续时间,推算出各个任务精确的起止时间了,甚至能够精确到秒。因此此模式用于时间规划,只是它的一个“副业”,引擎使用Chained Through Time模式时,并非直接对时间进行规划优化,而是在优化规划实体之间的链接关系;时间做为这个规划实体中的一个影子变量(Shadow variable)进行计算,最终经过评分机制对这个影子变量进行约束限制,从而获得时间优化的方案。与Time Slot和Time Grain相比,Chained Through Time最大的特性是经过次序来推导时间,而另外两种模式则是须要经过时间来反映任务之间的前后关系。
  虽然Chained Through Time模式的做用至关巨大且普遍,但该模式的设计与实现难度又是三个模式中最高的,实现起来相对复杂。下面来进一步对其进行深刻讨论。
 

Chained Through Time模式的意义

  Chained Through Time模式经过对正在进行规划的全部规划实体创建链状关系,来实现时间推导,其推导结果示意图以下。从图中能够看到,分配给Ann有两个任务(FR taxes和SP taxes),其中第一个任务FR taxes的开始时刻是固定为本次计划的最先时间,而第二个任务SP taxes的开始时刻,则是根据第一个任务推导出来的 - 等于第一个任务的开始时刻加上其持续时间。所以,须要在约束的限制下,引擎过过各类约束分数的判断,生成一个相对最合理的实体链接方案,再在这个方案的基础上来推导时间,或将时间归入做为约束条件,实现对链接方案的影响,从而实现了时间维度的规划优化。
 

 

 Chained Through Time的内存模型

  规划实体造成的链是由引擎自动生成的,每生成的一个方案都是由各规划实体之间的相对位置变化而成的。在建立的这些规划实体构成的链中,它会遵循如下原则:
  1. 一条链由一个Anchor(锚),和零或,或1个,或多个Entity(实体,其实就是规划实体)构成;
  2. 一条链必须有且仅有一个Anchor(锚);
  3. 一条链中的Entity或Anchor之间是一对一的关系,不可出现合流或分流结构;
  4. 一条链中的Entity或Anchor不可出现循环。
以下图
 

Chained Through Time模式的设计实现

  经过上面的链结构,咱们了解到,一条链中将会存在两种对象,一种是Anchor, 一种是Entity.对么它们分别表明现实场景中的什么业务实体呢?其实Entity是其常容易理解,若是是生产计划案例中,它表明的是每一个任务;在车辆路线规划案例中,它表明的是每一个车辆须要途径的派件/揽件客户。而Anchor则表未任务所在的机台,及各个投/揽方案中的每一车辆。所以,这两种不一样的对象,在内容中会造成依赖关系,即一个Entity的前一步能够是另一个Entiy, 也能够是一个Anchor。以生产计划的业务场景来描述,则表示一个任务的前一个任务,能够是另一个任务(Entity),也能够是一个机台(Anchor,当这个任务是这个机台的首个任务时)。所以,在咱们设计它的时候须要把这两种不一样的业务实体抽象为同一类才有办法实现它们之间的依赖,事实上这种抽象关系,在面向对象的原则,在业务意义上来讲,是不成立的,仅仅是为了知足它们造成同一链的要求才做出的计划。以下是一个任务与机台的类设计图。能够看到,我从Taskg与Machine抽象了一个父类Step(这是我想到的最合适类名了),那么每个任务的前一个Step有多是另一个任务,也有多是一个机台。
 

时间推算方法

  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邮件列表,国内网络可能较难访问,需自行解决)

相关文章
相关标签/搜索