2.2 业务模型建立

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

内容概要

今天我们来学习一个如何建立一个业务模型,有哪些概念是需要我们掌握的,例如:ProblemFact、PlanningEntity、ProblemSolution,以及这些对象中的字段有什么作用。

如何区分ProblemFact和PlanningEntity

我们来看一下规划问题的数据,会认识到其中的对象,每一个都可以被归类为以下的一种:

  • 无任何关联的类:不被任何分数约束所使用,从规划的角度来看,这个数据是垃圾数据。
  • Problem Fact事实类:由分数约束使用,但在求解期间不改变(只要问题保持不变)。比如说。床位、房间、班次、人员、时段….问题事实类的所有属性都是问题属性。
  • Planning Entity规划实体类:由分数约束使用,在求解期间改变。比如说。CloudProcess, TimeTable, Exam, ……在求解过程中改变的属性是Planning Variable规划变量,其他属性是问题属性。

问问自己,在求解期间,什么类会发生变化?哪个类有我想让求解器为我改变的变量?这个类就是一个Planning Entity规划实体。大多数用例只有一个规划实体类,大多数用例的每个规划实体类也只有一个规划变量。

在OptaPlanner中,所有Problem FactPlanning 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作为一个规划变量,在求解过程中会发生改变,而requiredCpuPowerrequiredMemoryrequiredNetworkBandwidth作为一个线程的其它信息,求解过程中不会发生改变。

@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的例子,它的比较器就是将所需资源(cpuPowermemorynetWorkBandwidth)更多的进程优先排在前面,让求解器可以优先解决这些需求比较大的进程。

当前的规划变量都不应该被用来比较规划实体的难度。在构建启发式方法期间,这些变量无论如何都可能是空的。例如,不应该使用CloudProcess中的computer

Planning variable规划变量

Planning variable注解

Planning Variable规划变量是Planning Entity上的一个JavaBean属性(所以是一个gettersetter)。它指向一个规划值,该值在规划过程中会发生变化。例如,CloudProcesscomputer属性就是一个真正的规划变量。高级情况下也可以有阴影变量,后续我们会学习到。

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规划变量的其它特性,为什么没有放在这里一起讲解,是因为下一篇章内容较多且需要有一定的基础才能理解,所以在大家学习完这三个重要的概念后,再来学习它的其它特性。

创作不易,禁止未授权的转载。如果我的文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享