OptaPlanner访问点跨车辆链切换时CustomShadowVariable未更新
问题背景
我们存在同一访问点同时完成新订单配送、旧订单取件的业务场景。针对该场景,我们新增了pickCapacity、deliveryCapacity字段,并在ArrivalTimeUpdatingVariableListener类中计算访问结束时的累积容量weightUsedAtEndOfVisit。
该逻辑整体运行正常,但当访问点在规划过程中从一条链(车辆)切换到另一条链时,原所属链上的weightUsedAtEndOfVisit值偶尔无法正常更新,相关实现代码如下:
TimeWindowedVisit 实体定义
@PlanningEntity public class TimeWindowedVisit{ protected Long dropWeight; protected Long pickupWeight; private Long weightUsedAtEndOfVisit; @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "arrivalTime")) public Long getWeightUsedAtEndOfVisit() { return weightUsedAtEndOfVisit; } public void setWeightUsedAtEndOfVisit(Long weightUsedAtEndOfVisit) { this.weightUsedAtEndOfVisit = weightUsedAtEndOfVisit; } }
ArrivalTimeUpdatingVariableListener 监听器实现
public class ArrivalTimeUpdatingVariableListener implements VariableListener<TimeWindowedPlanningVisit> { @Override public void afterEntityAdded(ScoreDirector scoreDirector, TimeWindowedPlanningVisit planningVisit) { updateUsedCapacityAtVisit(scoreDirector, planningVisit); } @Override public void afterVariableChanged(ScoreDirector scoreDirector, TimeWindowedPlanningVisit planningVisit) { updateUsedCapacityAtVisit(scoreDirector, planningVisit); } ... protected void updateUsedCapacityAtVisit(ScoreDirector<?> scoreDirector, TimeWindowedPlanningVisit sourcePlanningVisit) { TimeWindowedPlanningVisit shadowPlanningVisit; if (sourcePlanningVisit.getPreviousStandstill() != null) { shadowPlanningVisit = sourcePlanningVisit; while (shadowPlanningVisit != null) { if (shadowPlanningVisit.getPreviousStandstill() instanceof TimeWindowedPlanningVisit) { shadowPlanningVisit = (TimeWindowedPlanningVisit) shadowPlanningVisit.getPreviousStandstill(); } else { break; } } // 计算链上第一个站点,用于逐站累加容量 // 逐站传递累计容量,判断是否超过车辆载重上限 TimeWindowedPlanningVisit firstVisit = shadowPlanningVisit; // 从首站开始遍历,计算车辆从仓库出发时的初始载重 // 初始载重为所有需要配送的货物重量总和 long runningCapacity = 0; while (shadowPlanningVisit != null) { if (shadowPlanningVisit.isWarehousePickup()) { runningCapacity += shadowPlanningVisit.getDropWeight(); } shadowPlanningVisit = shadowPlanningVisit.getNextPlanningVisit(); } shadowPlanningVisit = firstVisit; // 已知初始出发载重后,逐站计算离开站点时的剩余载重 // 配送卸货扣减载重,取件装货增加载重 while (shadowPlanningVisit != null) { runningCapacity -= shadowPlanningVisit.getDropWeight(); runningCapacity += shadowPlanningVisit.getPickupWeight(); if (!Objects.equals(runningCapacity, shadowPlanningVisit.getWeightUsedAtEndOfVisit())) { scoreDirector.beforeVariableChanged(shadowPlanningVisit, "weightUsedAtEndOfVisit"); shadowPlanningVisit.setWeightUsedAtEndOfVisit(runningCapacity); scoreDirector.afterVariableChanged(shadowPlanningVisit, "weightUsedAtEndOfVisit"); } shadowPlanningVisit = shadowPlanningVisit.getNextPlanningVisit(); } } } }
容量约束实现
protected Constraint vehicleCapacityStopLevel(ConstraintFactory factory) { System.out.println("vehicleCapacityStopLevel..."); return factory.from(TimeWindowedPlanningVisit.class) .filter(visit -> visit.getWeightUsedAtEndOfVisit() > visit.getVehicle().getWeight()) .penalizeLong("vehicle capacity exceeded at stop", BendableLongScore.ofHard(2, 1, 0, 1), visit -> visit.getWeightUsedAtEndOfVisit() - visit.getVehicle().getWeight()); }
异常触发原因
- 核心逻辑缺失:当前更新逻辑只会遍历实体移动后所属的新链,从首站到尾站重算所有节点的容量值,完全没有处理实体被移走的旧链。当访问点从A链挪到B链时,监听器仅能收到B链上该实体的变更事件,只会重算B链容量;A链因为少了一个节点,后续所有站点的容量值都会变化,但没有任何逻辑触发A链的重算,必然残留旧值。
- 依赖绑定错误:
weightUsedAtEndOfVisit是和链结构强绑定的影子变量,取值直接依赖节点的前驱节点previousStandstill(决定节点所属车辆、前后顺序),但当前注解只绑定了arrivalTime的变化,会漏掉部分链结构调整但arrivalTime未同步触发更新的场景,导致更新不及时。 - 容量计算bug:计算车辆从仓库出发的初始载重时,仅累加了
isWarehousePickup() == true节点的配送重量,普通配送节点的待配送货物没有计入初始载重,会导致所有站点的容量计算值整体偏小。
修复方案
- 修正影子变量依赖:将
weightUsedAtEndOfVisit的监听对象从arrivalTime改为链式规划变量previousStandstill,保证所有链结构调整的事件都能被监听器捕获:@CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "previousStandstill")) public Long getWeightUsedAtEndOfVisit() { return weightUsedAtEndOfVisit; } - 补全旧链更新逻辑:在变量变更触发时,先通过
beforeVariableChanged钩子拿到实体移动前的旧前驱节点,顺着旧前驱找到旧链上的断点位置,先遍历重算旧链断点之后所有节点的容量值,再重算新链从插入点往后所有节点的容量值,不需要每次都从链首遍历,性能更好。 - 修正初始载重计算逻辑:计算车辆出发初始载重时,累加整条链所有节点的
dropWeight总和,去掉isWarehousePickup()的判断,避免漏算普通配送点的货物重量。 - 补充实体移除事件处理:重写监听器的
afterEntityRemoved方法,实体从链上移除时,主动触发所在链的容量重算,覆盖实体删除场景。
内容的提问来源于stack exchange,提问作者Vijay Kumar Y
相关产品推荐
相关产品推荐

