OptaPlanner examples その8

Vehicle routing


http://docs.jboss.org/optaplanner/release/latest/optaplanner-docs/html_single/index.html#vehicleRouting

複数のトラックで顧客を回り、集荷して倉庫に戻ります。巡回セールスマン問題のリアル版ですね。さらに TimeWindowedCustomer の場合(初期データ cvrptw-25customers.xml など)、「何時以降に来て」「何時以前に来て」「作業に一定時間がかかる」などの要素が追加されます。

  • VehicleRoutingSolution : @PlanningSolution
  • Standstill (実装クラスは Vehicle, Customer, TimeWindowedCustomer) : @PlanningEntity
    • Standstill previousStandstill : @PlanningVariable(graphType = PlanningVariableGraphType.CHAINED))
    • Location location
    • Customer nextCustomer : @InverseRelationShadowVariable(sourceVariableName = "previousStandstill")
    • Vehicle vehicle : @CustomShadowVariable

ルーティング問題なので、巡回セールスマンと同様、Standstill がひとつ前の Standstill を参照し、CHAINED の PlanningVariable とします。

今回の目玉は Shadow variables です。 http://docs.jboss.org/optaplanner/release/latest/optaplanner-docs/html_single/index.html#shadowVariable

Shadow variables とは、通常の PlanningVariable が変更された時に間接的に変更される(演繹される)プロパティです。ルールをより自然に書くためにそのようなメソッドを追加したくなったりします。

Standstill.java

    @InverseRelationShadowVariable(sourceVariableName = "previousStandstill")
    Customer getNextCustomer();

@InverseRelationShadowVariable をつけると、自分自身を sourceVariableName で参照しているオブジェクトを取得できます。これにより、PlanningVariable 同士が双方向で参照できます。

Customer.java

    @CustomShadowVariable(variableListenerClass = VehicleUpdatingVariableListener.class,
            sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
    public Vehicle getVehicle() {
        return vehicle;
    }

TimeWindowedCustomer.java

    @CustomShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class,
            sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
    public Integer getArrivalTime() {
        return arrivalTime;
    }

@CustomShadowVariable をつけると、自前のロジックで、指定の ShadowVariable の変更が可能です。variableListenerClass のクラスにロジックを記述します。

実際のところ、このような「間接的な計算結果を提供するメソッド」は、普通の Java プログラミングでは普通にメソッドに実装するだけで達成できます。ただ、Droolsでは「factのプロパティが変更された時」にはエンジンへの通知(kieSession.update())が必要になります。これは DroolsScoreDirector に実装されています。適切に DroolsScoreDirector.afterVariableChanged() に引っ掛けるためにアノテーションを使用しているというわけです。

    public void afterVariableChanged(VariableDescriptor variableDescriptor, Object entity) {
        update(entity);
        super.afterVariableChanged(variableDescriptor, entity);
    }

    private void update(Object entity) {
        FactHandle factHandle = kieSession.getFactHandle(entity);
        if (factHandle == null) {
            throw new IllegalArgumentException("The entity (" + entity
                    + ") was never added to this ScoreDirector."
                    + " Usually the cause is that that specific instance was not in your Solution's entities.");
        }
        kieSession.update(factHandle, entity);
    }