使用optaplanner进行仓选址

optaplanner是基于Java的运筹优化工具箱。OptaPlanner - The fast, Open Source and easy-to-use solver

它有丰富的例子和文档,但是中文资料很少。我不知道是不是因为它主要基于元启发算法而不是线性规划。由于最新版的optaplanner使用Java11和Stream,我估计国内使用的会更少。今天起我就来玩一下,哈哈。

通过实验和修改GitHub上的例子来学习optaplanner:GitHub - kiegroup/optaplanner-quickstarts: OptaPlanner quick starts for AI optimization: many use cases shown in many different technologies.

use-case目录下面有个facility-location。

optaplanner有趣的一点是跟Quarkus做了很好的集成,可以方便的部署成web应用。

在线性规划中,仓选址的模型变量会表示成向量或矩阵。optaplanner是基于面向对象的,所以有Consumer类、Facility类和Location类。Consumer的属性有location、facility和demand。Facility的属性有location、capacity、setupCost和consumer的List。Consumer和Facility类都需要加上@PlanningEntity这一注解。

生成随机数据的代码在bootstrap包下面。随机生成30个facility和60个consumer,总demand是900,总capacity是4500。setupCost的平均值是50000,标准差是10000。

类FacilityLocationProblem用来保存问题的解,所以它除了包含facility的List、consumer的List,还包含constraintConfiguration和score。

约束条件的配置在类FacilityLocationConstraintConfiguration中。约束条件有三个:硬约束"facility capacity",软约束"facility setup cost",软约束"distance from facility"。

约束条件具体规则在solver包下面的FacilityLocationConstraintProvider类中。约束条件的代码使用了Stream的函数式编程风格

    Constraint facilityCapacity(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Consumer.class)
                .groupBy(Consumer::getFacility, sumLong(Consumer::getDemand))
                .filter((facility, demand) -> demand > facility.getCapacity())
                .penalizeConfigurableLong(
                        FacilityLocationConstraintConfiguration.FACILITY_CAPACITY,
                        (facility, demand) -> demand - facility.getCapacity());
    }

    Constraint setupCost(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Consumer.class)
                .groupBy(Consumer::getFacility)
                .penalizeConfigurableLong(
                        FacilityLocationConstraintConfiguration.FACILITY_SETUP_COST,
                        Facility::getSetupCost);
    }

    Constraint distanceFromFacility(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Consumer.class)
                .filter(Consumer::isAssigned)
                .penalizeConfigurableLong(
                        FacilityLocationConstraintConfiguration.DISTANCE_FROM_FACILITY,
                        Consumer::distanceFromFacility);
    }

其实还是比较容易读懂的,但是实际写起来不那么容易。

数据和模型都有了,接下来就是求解和部署。由于optaplanner跟Quarkus集成,求解和部署比较紧密。它们在rest包下面。在SolverResource类里面,solve方法作为一个服务

    @POST
    @Path("solve")
    public void solve() {
        Optional maybeSolution = repository.solution();
        maybeSolution.ifPresent(facilityLocationProblem -> solverManager.solveAndListen(
                PROBLEM_ID,
                id -> facilityLocationProblem,
                repository::update,
                (problemId, throwable) -> solverError.set(throwable)));
    }

实际上是用SolverManager来求解的,SolverManager会使用线程池。

前端部分,可以在resources/META_INFO.resources/看到。用到了leaflet.js。如果想用高德地图,可以把app.js第232行的'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'改为'http://map.geoq.cn/ArcGIS/rest/services/ChinaOnlineCommunity/MapServer/tile/{z}/{y}/{x}'。为了换成中国的城市,要改一下229行的setView里面的经纬度。

最后,尝试一下修改约束条件。假如我们想限制仓的个数,该怎么做?

首先,在FacilityLocationConstraintConfiguration中添加约束名称和HardSoftLongScore,例如"count of facilities"。这里需要添加两个约束:仓数的上限和下限。

具体规则在类FacilityLocationConstraintProvider中添加:

    // 惩罚多于4个仓
    Constraint facilityCnt(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Consumer.class)
                .groupBy(Consumer::getFacility)
                .distinct()
                .groupBy(count())
                .filter(c -> c >= 4)
                .penalizeConfigurable(
                        FacilityLocationConstraintConfiguration.FACILITY_CNT,
                        c -> c - 4);
    }

    // 惩罚少于3个仓
    Constraint facilityCntMin(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Consumer.class)
                .groupBy(Consumer::getFacility)
                .distinct()
                .groupBy(count())
                .filter(c -> c <= 3)
                .penalizeConfigurableLong(
                        FacilityLocationConstraintConfiguration.FACILITY_CNT_MIN,
                        c -> 3 - c);
    }

注意在添加惩罚分之前要先filter,否则会报错。思路是按facility去重后计数。这里限制是至少3各仓,至多4个仓。

结果如下使用optaplanner进行仓选址_第1张图片

 日志如下:

2022-09-19 22:55:26,769 INFO  [org.opt.cor.imp.sol.DefaultSolver] (pool-19-thread-1) Solving ended: time spent (30001), best score (0hard/-901900soft), score calculation speed (28046/s
ec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE).
 

你可能感兴趣的:(java)