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个仓。
日志如下:
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).