Explaining OnRoute results
This section is a technically in-depth version of Explainability and functions as a developer guide. Frequently asked questions for routing might be caused by visual intuitions such as: why is this vehicle crossing its own route, or why isn't it more logical to use this pattern rather than the one the solver came up with? Everything regarding quality will have one key idea linked to it: the solution score. Using the score as a reference in combination with the /evaluate
endpoint - which we'll introduce in the next paragraphs - a developer can find out all the answers (given that the Explainability part was also read).
Evaluation flow
We'll work using a request based on the /demo endpoint for PVRP. This solve
request below (first tab) is solved using the flow described in the Solve document. From the job response that we get, we distill the score info and the 'solution' node, which offers all information regarding the schedule of the fleet (second and third tab). The solution node is rather verbose, so that for instance a front-end can be built on top of it. It's that same solution node that we'll use to feed the solver, however, it can be represented in a leaner way. The lean representation is shown in the fourth tab.
{
"solver": "PVRP",
"locations": [
{
"name": "loc0",
"latitude": 51.132042753741956,
"longitude": 3.7120073316457938
},
{
"name": "loc1",
"latitude": 51.19384837423016,
"longitude": 3.626153117855758
}
],
"fleet": [
{
"name": "truck0",
"startlocation": "loc0",
"endlocation": "loc1",
"shiftstart": 480,
"shiftend": 800
}
],
"orders": [
{
"name": "order0",
"location": "loc0",
"duration": 30
},
{
"name": "order1",
"location": "loc1",
"duration": 30
}
],
"period": {
"start": "2022-10-17",
"end": "2022-10-18"
}
}
{
"score": {
"initScore": 0,
"hardScore": 0,
"mediumScore": 0,
"softScore": -19,
"feasible": true,
"solutionInitialized": true,
"zero": false
},
"unresolved": [
{
"name": "Travel Time",
"value": -19,
"level": "SOFT",
"notZero": true
}
]
}
{
"solution": {
"truck0": {
"2022-10-17": [
{
"location": "loc0",
"arrival": 480,
"finish": 480,
"wait": 0,
"drive": 0,
"distance": 0,
"service": 0,
"demand": 0,
"demand2": 0,
"coords": {
"address": "loc0",
"latitude": 51.132042753741956,
"longitude": 3.7120073316457938
},
"date": "2022-10-17"
},
{
"location": "loc0",
"order": "order0",
"nextLocation": "loc1",
"arrival": 480,
"finish": 510,
"wait": 0,
"drive": 0,
"distance": 0,
"service": 30,
"demand": 0,
"demand2": 0,
"coords": {
"address": "loc0",
"latitude": 51.132042753741956,
"longitude": 3.7120073316457938
},
"date": "2022-10-17"
},
{
"location": "loc1",
"order": "order1",
"nextLocation": "loc1",
"arrival": 529,
"finish": 559,
"wait": 0,
"drive": 19,
"distance": 11777,
"service": 30,
"demand": 0,
"demand2": 0,
"coords": {
"address": "loc1",
"latitude": 51.19384837423016,
"longitude": 3.626153117855758
},
"date": "2022-10-17"
},
{
"location": "loc1",
"arrival": 559,
"finish": 559,
"wait": 0,
"drive": 0,
"distance": 0,
"service": 0,
"demand": 0,
"demand2": 0,
"coords": {
"address": "loc1",
"latitude": 51.19384837423016,
"longitude": 3.626153117855758
},
"date": "2022-10-17"
}
]
}
}
}
{
"solution": {
"truck0": {
"2022-10-17": [
{
"order": "order0"
},
{
"order": "order1"
}
]
}
}
}
Surely, it's possible to use the more verbose input as well, however the solver doesn't need it. The lean representation of the solution is simply more developer friendly when solutions get tweaked or when they are built up from the ground and it suffices as input.
Below, rather than a solve
request, we create an evaluate
request. The flow is entirely analogous, with the internal difference that the solution you pass in the request is interpreted as a fact. Not changing the order sequence should give the same score as before when you GET
this evaluate
job.
HTTP POST https://api.solvice.io/evaluate
Let's say we think the orders are better off when they're swapped, so taking order1 before order0.
{
"solution": {
"truck0": {
"2022-10-17": [
{
"order": "order1"
},
{
"order": "order0"
}
]
}
}
}
{
"solver": "PVRP",
"locations": [
{
"name": "loc0",
"latitude": 51.132042753741956,
"longitude": 3.7120073316457938
},
{
"name": "loc1",
"latitude": 51.19384837423016,
"longitude": 3.626153117855758
}
],
"fleet": [
{
"name": "truck0",
"startlocation": "loc0",
"endlocation": "loc1",
"shiftstart": 480,
"shiftend": 800
}
],
"orders": [
{
"name": "order0",
"location": "loc0",
"duration": 30
},
{
"name": "order1",
"location": "loc1",
"duration": 30
}
],
"period": {
"start": "2022-10-17",
"end": "2022-10-18"
},
"solution": {
"truck0": {
"2022-10-17": [
{
"order": "order1"
},
{
"order": "order0"
}
]
}
}
}
{
"score": {
"initScore": 0,
"hardScore": 0,
"mediumScore": 0,
"softScore": -57,
"feasible": true,
"solutionInitialized": true,
"zero": false
},
"unresolved": [
{
"name": "Travel Time",
"value": -38,
"level": "SOFT",
"notZero": true
},
{
"name": "End Location Travel Time",
"value": -19,
"level": "SOFT",
"notZero": true
}
]
}
This total score is three times as big as the original score, which is logical, since the underlying route (taking 19 minutes) is the same, but there's an extra back-and-forth between the locations (38 minutes). We also note that in the evaluation there's a specific "End Location Travel Time". The reason is that during the initial solve, the last order served (order1) coincides with the end location of the vehicle (loc1). Now, the vehicle has to drive to loc1 after coming from order0 at loc0, which was not the case before.
Using this flow is at the core of understanding and debugging solutions. The example question of a vehicle crossing its own route can be answered by changing a solution so that it is 'uncrossed'. Just like introduced in the Explainability, there are three categories: evaluating another solution is score-wise better, equal or worse. We'll introduce a few frequently asked questions.
FAQ
Q: Locations that are close to each other are being served in a weird way. However, scores are the same and the types of cost (Travel Time etc.) are also the same. Why isn't the solver offering a more logical solution?
A: The score is all the solver has to go by. If there isn't any score benefit to another solution, there is no internal reason to choose it, even though for humans it might seem more logical to do so. Possible reasons are: using minute granularity instead of seconds which rounds scores or not using squash durations (relates to setup cost) so that there is no reason to group orders at the same location if that location is passed again later on. We're glad to explain our feature set.
Q: I only swapped these two locations and all of a sudden, the score got way worse. Why is that?
A: That depends, the factors are listed under the "unresolved" node of the response. Known causes are traffic direction, making a swap of locations very asymmetric, as well as delivery time window violations. Perhaps this happens in combination with violating the shift end of a driver. Checking the "unresolved" reporting of the score gives insight into the specifics of the case.
Q: We took a look at the generated routes and found some weird routes. An evaluation showed us that we found a better scored routing schedule than the solver was able to. How would a qualitative solver come to this result?
A: These problems are computationally hard, the more time the solver gets, the better the results can get. You could consider upping the solve duration by appending ?seconds=x to the solve url. If reducing artifacts is very important, consider a two phased approach in which Long Term Routing is calculated first and is post-processed by Single Day Vehicle Routing requests. The solver spends the bulk of its time finding the best improvements with the time it has. It could optimise for small post-processing improvements, reducing the chance that the human eye finds something better rather fast, but also reducing the overall quality of the solution.
Updated about 2 years ago