这是我参与更文挑战的第5天,活动详情查看: 更文挑战
内容概要
今天我们来学习一个如何建立一个业务模型,有哪些概念是需要我们掌握的,例如:ProblemFact、PlanningEntity、ProblemSolution,以及这些对象中的字段有什么作用。
如何区分ProblemFact和PlanningEntity
我们来看一下规划问题的数据,会认识到其中的对象,每一个都可以被归类为以下的一种:
- 无任何关联的类:不被任何分数约束所使用,从规划的角度来看,这个数据是垃圾数据。
- Problem Fact事实类:由分数约束使用,但在求解期间不改变(只要问题保持不变)。比如说。床位、房间、班次、人员、时段….问题事实类的所有属性都是问题属性。
- Planning Entity规划实体类:由分数约束使用,在求解期间改变。比如说。
CloudProcess
,TimeTable
,Exam
, ……在求解过程中改变的属性是Planning Variable规划变量,其他属性是问题属性。
问问自己,在求解期间,什么类会发生变化?哪个类有我想让求解器为我改变的变量?这个类就是一个Planning Entity规划实体。大多数用例只有一个规划实体类,大多数用例的每个规划实体类也只有一个规划变量。
在OptaPlanner中,所有Problem Fact
和Planning Entity
都是普通的JavaBeans(POJOs)。
Problem Fact
Problem Fact
问题事实类是任何带有getter
方法的JavaBean(POJO),在求解过程中不会改变。例如,云资源优化例子中,Computer
是Problem Fact问题事实。
@XStreamAlias("CloudComputer")
public class CloudComputer extends AbstractPersistable implements Labeled {
private int cpuPower; // in gigahertz
private int memory; // in gigabyte RAM
private int networkBandwidth; // in gigabyte per hour
private int cost; // in euro per month
public CloudComputer() {
}
public CloudComputer(long id, int cpuPower, int memory, int networkBandwidth, int cost) {
super(id);
this.cpuPower = cpuPower;
this.memory = memory;
this.networkBandwidth = networkBandwidth;
this.cost = cost;
}
public int getCpuPower() {
return cpuPower;
}
public void setCpuPower(int cpuPower) {
this.cpuPower = cpuPower;
}
public int getMemory() {
return memory;
}
public void setMemory(int memory) {
this.memory = memory;
}
public int getNetworkBandwidth() {
return networkBandwidth;
}
public void setNetworkBandwidth(int networkBandwidth) {
this.networkBandwidth = networkBandwidth;
}
public int getCost() {
return cost;
}
public void setCost(int cost) {
this.cost = cost;
}
// ************************************************************************
// Complex methods
// ************************************************************************
public int getMultiplicand() {
return cpuPower * memory * networkBandwidth;
}
@Override
public String getLabel() {
return "Computer " + id;
}
}
复制代码
当然,一个问题事实可以参包含他问题事实。
public class Course {
private String code;
private Teacher teacher; // Other problem fact
private int lectureSize;
private int minWorkingDaySize;
private List<Curriculum> curriculumList; // Other problem facts
private int studentSize;
// ... getters
}
复制代码
一个问题事实类不需要任何OptaPlanner的注解。
Planning entity
Planning entity annotation
Planning Entity规划实体
是一个JavaBean(POJO),在求解过程中会发生变化,例如一个CloudProcess
会换成另一台Computer
。一个规划问题有多个规划实体,例如对于云资源优化问题,每个CloudProcess
都是一个规划实体。但通常只有一个规划实体类,例如CloudProcess类
。
一个规划实体类,需要增加@PlanningEntity
注解。
每个规划实体类有一个或多个Planning Variable规划变量
。它还应该有一个或多个定义性的属性。例如,CloudProcess
类中,computer
作为一个规划变量,在求解过程中会发生改变,而requiredCpuPower
,requiredMemory
,requiredNetworkBandwidth
作为一个线程的其它信息,求解过程中不会发生改变。
@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
@XStreamAlias("CloudProcess")
public class CloudProcess extends AbstractPersistable {
private int requiredCpuPower; // in gigahertz
private int requiredMemory; // in gigabyte RAM
private int requiredNetworkBandwidth; // in gigabyte per hour
// Planning variables: 规划变量:规划期间的变化,分数计算之间的变化。
private CloudComputer computer;
......
}
复制代码
一个规划实体类可以有多个规划变量。例如课程时间表例子,一节课程,是有老师在某个时间段上在某个教室进行授课,所以Lesson
它有两个规划变量,timeslot
时段和room
教室。
@PlanningEntity
public class Lesson {
private Long id;
private String subject;
private String teacher;
private String studentGroup;
@PlanningVariable(valueRangeProviderRefs = "timeslotRange")
private Timeslot timeslot;
@PlanningVariable(valueRangeProviderRefs = "roomRange")
private Room room;
....
}
复制代码
求解器配置需要声明每个规划实体类。
<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
...
<entityClass>org.optaplanner.examples.cloudbalancing.domain.CloudProcess</entityClass>
...
</solver>
复制代码
Planning Entity比较器
一些优化算法如果能估算出哪些规划实体更难规划,就能更有效地工作。例如:在垃圾箱包装中,更大的物品更难装入,在课程安排中,有更多学生的讲座更难安排,而在n个皇后中,中间的皇后更放在棋盘上。
不要尝试使用Comparator比较器来实现业务约束。它不会影响评分函数:如果我们有无限的求解时间,返回的解决方案将是一样的。
为了达到某些实体在日程表中被安排得更早的目的,添加一个分数约束来改变分数函数,使其更倾向于这种解决方案。只有在可以使求解器更有效率的情况下,才考虑也增加规划实体的Comparator。
为了让启发式方法能够利用当前优化问题的特殊信息,需要@PlanningEntity
注解设置 difficultyComparatorClass
属性。
@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
public class CloudProcess {
// ...
}
复制代码
public class CloudProcessDifficultyComparator implements Comparator<CloudProcess> {
public int compare(CloudProcess a, CloudProcess b) {
return new CompareToBuilder()
.append(a.getRequiredMultiplicand(), b.getRequiredMultiplicand())
.append(a.getId(), b.getId())
.toComparison();
}
}
复制代码
比较器应该从高到低实现:容易的实体比较低,困难的实体比较高。例如,在垃圾箱包装中:小物品<中物品<大物品。
虽然大多数算法都是先从较难的实体开始,但它们只是把顺序颠倒过来。
针对云平衡CloudBalance
的例子,它的比较器就是将所需资源(cpuPower
、memory
、netWorkBandwidth
)更多的进程优先排在前面,让求解器可以优先解决这些需求比较大的进程。
当前的规划变量都不应该被用来比较规划实体的难度。在构建启发式方法期间,这些变量无论如何都可能是空的。例如,不应该使用CloudProcess
中的computer
。
Planning variable规划变量
Planning variable注解
Planning Variable
规划变量是Planning Entity
上的一个JavaBean属性(所以是一个getter
和setter
)。它指向一个规划值,该值在规划过程中会发生变化。例如,CloudProcess
的computer
属性就是一个真正的规划变量。高级情况下也可以有阴影变量,后续我们会学习到。
Planning Variable
规划变量getter
方法需要添加@PlanningVariable
注解,并且需要一个非空的valueRangeProviderRefs
属性。
@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
@XStreamAlias("CloudProcess")
public class CloudProcess extends AbstractPersistable {
private int requiredCpuPower; // in gigahertz
private int requiredMemory; // in gigabyte RAM
private int requiredNetworkBandwidth; // in gigabyte per hour
// Planning variables: changes during planning, between score calculations.
private CloudComputer computer;
......
@PlanningVariable(valueRangeProviderRefs = {
"computerRange" }, strengthComparatorClass = CloudComputerStrengthComparator.class)
public CloudComputer getComputer() {
return computer;
}
public void setComputer(CloudComputer computer) {
this.computer = computer;
}
......
}
复制代码
valueRangeProviderRefs
属性定义了此规划变量的可能规划值。它引用了一个或多个@ValueRangeProvider id
。
@PlanningVariable注解需要在具有@PlanningEntity注解的类中的属性上。它在没有该注解的父类或子类中被忽略。
也可以将注解添加在字段上:
@PlanningEntity
public class Queen {
...
@PlanningVariable(valueRangeProviderRefs = {"rowRange"})
private Row row;
}
复制代码
规划变量允许为空
默认情况下,一个初始化的规划变量不能为空,所以一个初始化的解决方案永远不会对其任何规划变量使用空。在一个其它用例中,这可能会产生反作用。例如:在进行任务分配时,我们宁愿让低优先级的任务不被分配,而不是把它们分配给一个超负荷的工人。
如果要允许一个初始化的规划变量为空,请将nullable
设置为true
。
@PlanningVariable(..., nullable = true)
public Worker getWorker() {
return worker;
}
复制代码
ConstraintProvider约束流实现时,默认会过滤掉具有null规划变量的规划实体。使用fromUnfiltered()可以避免这种情况。
private UniConstraintStream<RockShow> getShowWithoutDate(ConstraintFactory constraintFactory) {
return constraintFactory.fromUnfiltered(RockShow.class)
.filter(rockShow -> rockShow.getDate() == null);
}
复制代码
OptaPlanner会自动将数值null
添加到数值范围中。无需在一个由ValueRangeProvider
提供的List
中添加null
。
使用一个允许为null的规划变量意味着我们的分数计算要负责惩罚(甚至奖励)有null值的变量。
总结
这一篇章主要学习了OptaPlanner的Problem Fact和Planning Entity以及Planning Variable的概念,及如何在OptaPlanner去使用它。
作业
大家可以回过头来,看看之前的两个例子,来加深对这些概念的理解,这三个概念是贯穿整个OptPlanner最核心的三个概念,一定要真正的去掌握和弄懂。
结束语
下一篇章,我们会着重讲解Planning variable规划变量的其它特性,为什么没有放在这里一起讲解,是因为下一篇章内容较多且需要有一定的基础才能理解,所以在大家学习完这三个重要的概念后,再来学习它的其它特性。
创作不易,禁止未授权的转载。如果我的文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤