You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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节点的配送重量,普通配送节点的待配送货物没有计入初始载重,会导致所有站点的容量计算值整体偏小。
修复方案
  1. 修正影子变量依赖:将weightUsedAtEndOfVisit的监听对象从arrivalTime改为链式规划变量previousStandstill,保证所有链结构调整的事件都能被监听器捕获:
    @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "previousStandstill"))
    public Long getWeightUsedAtEndOfVisit() {
        return weightUsedAtEndOfVisit;
    }
    
  2. 补全旧链更新逻辑:在变量变更触发时,先通过beforeVariableChanged钩子拿到实体移动前的旧前驱节点,顺着旧前驱找到旧链上的断点位置,先遍历重算旧链断点之后所有节点的容量值,再重算新链从插入点往后所有节点的容量值,不需要每次都从链首遍历,性能更好。
  3. 修正初始载重计算逻辑:计算车辆出发初始载重时,累加整条链所有节点的dropWeight总和,去掉isWarehousePickup()的判断,避免漏算普通配送点的货物重量。
  4. 补充实体移除事件处理:重写监听器的afterEntityRemoved方法,实体从链上移除时,主动触发所在链的容量重算,覆盖实体删除场景。

内容的提问来源于stack exchange,提问作者Vijay Kumar Y

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 08:44:04