Analyzing Unscheduled Orders
This sample describes how to analyze the reason, why an order remains unscheduled by the xTour service.
Benefits
- Users learn how to use and integrate the operations of xTour that analyze unscheduled orders.
Prerequisites
Please ensure following prerequisites are fulfilled before you start with the use case:
- Installed and licensed PTV xTour service
- License for as many as the plan should contain
Concepts
Programming Guide
This example provides information on how to analyze unscheduled orders to find out why they remained unplanned or if the orders are unplannable.
After passing a PlanToursRequest
to the planTours
operation of the xTour service, a number of orders may remain unplanned.
As preparation for the analysis of the unscheduled orders, the plan must be stored using the field storeRequest
in the PlanToursRequest
.
The analysis itself depends on whether the orders are unplanned or unplannable.
Analyze unplanned orders
For an unplanned order whose id is in orderIdsNotPlanned
, but not in orderIdsNotPlannable
, we can simply send an InsertionPositionsForOrdersQuery
.
The field orderIds
contains the id of the order to analyze.
Note that only one order can be analyzed at a time.
If you leave targetVehicleIds
empty, then all possible insertion positions for all vehicles are proposed.
As we expect all insertion positions to be violated, returnViolatedTours
has to be set to true
.
The resulting proposals contain MoveOrdersActions
and AddTripActions
.
Each proposal only contains the changed tour.
The corresponding TourViolationReport
contains the violations occurring when inserting the order at the given position.
We can use this report to find out what prevented the order from being scheduled.
As the order is unplanned but not unplannable, an additional vehicle would always be a feasible way to schedule the order.
The following sample contains a simple way of aggregating the violations in the proposals to find out whether the planning horizon or the opening intervals are the reason for an order being unplanned.
You can modify the initial plan by changing useShortPlanningHorizon
:
- Set
useShortPlanningHorizon
to true
, so that an order cannot be planned because of the planning horizon being too short. - Set
useShortPlanningHorizon
to false
, so that an order cannot be planned because of an opening interval being too short.
function getPlanToursRequest() {
// If this is set to false, a longer planning horizon is used, but the time windows are shorter.
var useShortPlanningHorizon = true;
var depotLocation = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.1035919189453125,
"y": 49.61070993807422
}
};
var customerLocation1 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.147022247314454,
"y": 49.59685949756711
}
};
var customerLocation2 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.115522384643555,
"y": 49.603701773184035
}
};
var customerLocation3 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.156635284423828,
"y": 49.616048816070446
}
};
var locations = [
{
"$type": "DepotSite",
"id": "Depot",
"routeLocation" : depotLocation
},
{
"$type": "CustomerSite",
"id": "Customer1",
"routeLocation" : customerLocation1,
"openingIntervals": [
{
"$type": "StartEndInterval",
"start": "2022-01-01T10:00:00+01:00",
"end": useShortPlanningHorizon ? "2022-01-01T18:00:00+01:00" : "2022-01-01T11:00:00+01:00"
}
]
},
{
"$type": "CustomerSite",
"id": "Customer2",
"routeLocation" : customerLocation2,
"openingIntervals": [
{
"$type": "StartEndInterval",
"start": "2022-01-01T11:00:00+01:00",
"end": useShortPlanningHorizon ? "2022-01-01T18:00:00+01:00" : "2022-01-01T12:00:00+01:00"
}
]
},
{
"$type": "CustomerSite",
"id": "Customer3",
"routeLocation" : customerLocation3,
"openingIntervals": [
{
"$type": "StartEndInterval",
"start": "2022-01-01T12:00:00+01:00",
"end": useShortPlanningHorizon ? "2022-01-01T18:00:00+01:00" : "2022-01-01T13:00:00+01:00"
}
]
},
];
var orders = [
{
"$type": "PickupDeliveryOrder",
"id": 1,
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer1",
"serviceTimeForDelivery": 2 * 60 * 60, // 2 hours
},
{
"$type": "PickupDeliveryOrder",
"id": 2,
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer2",
"serviceTimeForDelivery": 2 * 60 * 60, // 2 hours
},
{
"$type": "PickupDeliveryOrder",
"id": 3,
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer3",
"serviceTimeForDelivery": 2 * 60 * 60, // 2 hours
}
];
var planToursRequest = {
"locations": locations,
"orders": orders,
"fleet": {
"vehicles": [
{
"ids": [ "vehicle1" ]
}
]
},
"planToursOptions": {
"planningHorizon": {
"start": "2022-01-01T10:00:00+01:00",
"end": useShortPlanningHorizon ? "2022-01-01T15:00:00+01:00" : "2022-01-01T18:00:00+01:00"
}
},
"distanceMode": {
"$type": "DirectDistance"
},
"storeRequest": true
};
return planToursRequest;
}
function planToursAndAnalyzeUnplannedOrder() {
var planToursRequest = getPlanToursRequest();
xtour.planTours(
planToursRequest,
function(toursResponse, exception) {
analyzeUnplannedOrders(planToursRequest, toursResponse);
}
);
}
function analyzeUnplannedOrders(planToursRequest, toursResponse) {
if (toursResponse.orderIdsNotPlanned.length > 0)
{
var unplannedOrderId = toursResponse.orderIdsNotPlanned[0];
var proposalsQuery = {
"$type": "InsertionPositionsForOrdersQuery",
"storedRequestId": toursResponse.storedRequestId,
"orderIds": [ unplannedOrderId ],
"targetVehicleIds": []
};
var findChangeToursProposalsRequest = {
"proposalsQuery": proposalsQuery,
"proposalsOptions": {
"returnViolatedTours": true
}
};
xtour.findChangeToursProposals(findChangeToursProposalsRequest,
function(changeToursProposalsResponse, exception) {
analyzeProposals(unplannedOrderId, changeToursProposalsResponse);
}
);
}
else
{
print('All orders were scheduled.');
}
}
function analyzeProposals(unplannedOrderId, changeToursProposalsResponse) {
var numberOfProposalsWithPlanningHorizonExceedance = 0;
var numberOfProposalsWithOpeningIntervalExceedance = 0;
for (var index = 0; index < changeToursProposalsResponse.proposals.length; index++) {
if (changeToursProposalsResponse.proposals[index].tourReports[0].violationReport.planningHorizonExceedance > 0) {
numberOfProposalsWithPlanningHorizonExceedance++;
}
if (changeToursProposalsResponse.proposals[index].tourReports[0].violationReport.maximumOpeningIntervalExceedance > 0) {
numberOfProposalsWithOpeningIntervalExceedance++;
}
}
if (numberOfProposalsWithPlanningHorizonExceedance > 0) {
print('An additional vehicle or a longer planning horizon is needed for order ' + unplannedOrderId + ' to be planned.');
} else if (numberOfProposalsWithOpeningIntervalExceedance > 0) {
print('An additional vehicle or longer opening intervals are needed for order ' + unplannedOrderId + ' to be planned.');
}
}
planToursAndAnalyzeUnplannedOrder();
Analyze unplannable orders
Contrary to the previous part, unplannable orders cannot be served using additional vehicles.
For a reasonable analysis, we need a plan without tours, so that we can find out the reason for the order being unplannable even if the vehicles do not serve any other order.
In the following sample, the request contains an order with a 20-hours service time.
Even a completely empty vehicle cannot serve the order, because the planning horizon only has a length of 8 hours.
After planning, the id of the unplannable order is in orderIdsNotPlannable
.
In function createSingleOrderPlan
a plan is created which only contains the unplannable order.
Function analyzeUnplannableOrders
then runs an InsertionPositionsForOrdersQuery
like before.
Only one AddTripAction
per vehicle is created.
We again have a look at the TourViolationReport
to find out the reason for the order to be unplannable, which in this example is the planning horizon being too short.
function getPlanToursRequest() {
var depotLocation = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.1035919189453125,
"y": 49.61070993807422
}
};
var customerLocation1 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.147022247314454,
"y": 49.59685949756711
}
};
var customerLocation2 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.115522384643555,
"y": 49.603701773184035
}
};
var customerLocation3 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.156635284423828,
"y": 49.616048816070446
}
};
var locations = [
{
"$type": "DepotSite",
"id": "Depot",
"routeLocation" : depotLocation
},
{
"$type": "CustomerSite",
"id": "Customer1",
"routeLocation" : customerLocation1,
"openingIntervals": [
{
"$type": "StartEndInterval",
"start": "2022-01-01T10:00:00+01:00",
"end": "2022-01-01T18:00:00+01:00"
}
]
},
{
"$type": "CustomerSite",
"id": "Customer2",
"routeLocation" : customerLocation2,
"openingIntervals": [
{
"$type": "StartEndInterval",
"start": "2022-01-01T10:00:00+01:00",
"end": "2022-01-01T18:00:00+01:00"
}
]
},
{
"$type": "CustomerSite",
"id": "Customer3",
"routeLocation" : customerLocation3,
"openingIntervals": [
{
"$type": "StartEndInterval",
"start": "2022-01-01T10:00:00+01:00",
"end": "2022-01-01T18:00:00+01:00"
}
]
},
];
var orders = [
{
"$type": "PickupDeliveryOrder",
"id": 1,
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer1",
"serviceTimeForDelivery": 2 * 60 * 60, // 2 hours
},
{
"$type": "PickupDeliveryOrder",
"id": 2,
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer2",
"serviceTimeForDelivery": 2 * 60 * 60, // 2 hours
},
{
"$type": "PickupDeliveryOrder",
"id": 3,
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer3",
"serviceTimeForDelivery": 20 * 60 * 60, // 20 hours!
}
];
var planToursRequest = {
"locations": locations,
"orders": orders,
"fleet": {
"vehicles": [
{
"ids": [ "vehicle1" ]
}
]
},
"planToursOptions": {
"planningHorizon": {
"start": "2022-01-01T10:00:00+01:00",
"end": "2022-01-01T18:00:00+01:00"
}
},
"distanceMode": {
"$type": "DirectDistance"
},
"storeRequest": true
};
return planToursRequest;
}
function planToursAndAnalyzeUnplannableOrder() {
var planToursRequest = getPlanToursRequest();
xtour.planTours(
planToursRequest,
function(toursResponse, exception) {
createSingleOrderPlan(planToursRequest, toursResponse);
}
);
}
function createSingleOrderPlan(planToursRequest, toursResponse) {
if (toursResponse.orderIdsNotPlannable.length > 0)
{
var unplannableOrderId = toursResponse.orderIdsNotPlannable[0];
var singleOrderPlanToursRequest = planToursRequest;
singleOrderPlanToursRequest.orders = [
planToursRequest.orders.find(order => { return order.id == unplannableOrderId; })
];
xtour.planTours(
singleOrderPlanToursRequest,
function(singleOrderToursResponse, exception) {
analyzeUnplannableOrders(unplannableOrderId, singleOrderPlanToursRequest, singleOrderToursResponse);
}
);
}
else
{
print('No order is unplannable.');
}
}
function analyzeUnplannableOrders(unplannableOrderId, singleOrderPlanToursRequest, singleOrderToursResponse) {
var proposalsQuery = {
"$type": "InsertionPositionsForOrdersQuery",
"storedRequestId": singleOrderToursResponse.storedRequestId,
"orderIds": [ unplannableOrderId ],
"targetVehicleIds": []
};
var findChangeToursProposalsRequest = {
"proposalsQuery": proposalsQuery,
"proposalsOptions": {
"returnViolatedTours": true
}
};
xtour.findChangeToursProposals(findChangeToursProposalsRequest,
function(changeToursProposalsResponse, exception) {
analyzeProposals(unplannableOrderId, changeToursProposalsResponse);
}
);
}
function analyzeProposals(unplannableOrderId, changeToursProposalsResponse) {
var numberOfProposalsWithPlanningHorizonExceedance = 0;
for (var index = 0; index < changeToursProposalsResponse.proposals.length; index++) {
if (changeToursProposalsResponse.proposals[index].tourReports[0].violationReport.planningHorizonExceedance > 0) {
numberOfProposalsWithPlanningHorizonExceedance++;
}
}
if (numberOfProposalsWithPlanningHorizonExceedance > 0) {
print('A longer planning horizon is needed for order ' + unplannableOrderId + ' to be planned.');
}
}
planToursAndAnalyzeUnplannableOrder();
Limiting the resulting number of proposals of an InsertionPositionsForOrdersQuery
Running an InsertionPositionsForOrdersQuery
yields all possible insertion positions for the given order.
While the number of AddTripActions
is limited by the number of vehicles and existing trips, many more MoveOrdersActions
may be proposed, depending on the size of the plan.
Therefore, you can limit the number of MoveOrdersActions
using the field maximumNumberOfMoveOrdersActions
so that at most the given number of nearest insertion positions are returned.
Note
We recommend not to exceed the default value of 100, as a larger number hardly increases the quality of the proposals, but increases the computation time and the response size.
Additionally, you can limit the proposed MoveOrdersActions
using maximumDistanceOfAdjacentStops
to a certain area around the customer site of the given order (or in case of a depot-depot transport to an area around both depot sites).
Only stops in this area may be used as neighboring stops for the insertion.