Closed juanluisrto closed 1 year ago
I am not sure if I agree. Vroom is designed to optimize on the problem here and now, not on some potential larger question it doesn't know about yet. I'm not surpriced at all that sometimes the cost is a little higher like that.
That said, you are right that this is a vroom issue. @jcoupey do you want to add your two cents?
First, in principle there is no guarantee that you'll get normal_vrp_cost <= split_vrp_cost
. This could indeed sound weird since the "normal" instance does have a more global view of the problem, but you'd only get this kind of assurance if we were to provide guaranteed optimal solutions.
That said, I suspect there is something else going on here related to the specific setup you're trying.
I define a set of jobs at random locations with random weights
Do you mean capacity restriction by "weights"? In your code there is no constraint whatsoever so basically there is nothing preventing any vehicle to handle all jobs in a very long route. Thus I'd be surprised if jobs_left
is not empty for your second solving round.
Surely not related to pyvroom per se and its more on the vroom side
Yes. Is there a convenient way to serialize the problem instances to the "usual" json format from pyvroom
? That would make things easier to actually look at the instances and reproduce.
Pyvroom supports reading json format on input, and writing json for solutions. It does not support writing json for input. Is that supported in upstream vroom?
Thanks for the insights to both of you, I understand that there are no guarantees of getting more optimal solutions with more vehicles. It just seemed to me weird enough to flag it.
Regarding the capacity restrictions, forget what I said. I removed those constraints and realized this behaviour still occurred (also removed max_travel_time
), so now I'm just running unconstrained VRPs.
@jcoupey Its true that jobs_left was many times empty, unless the jobs were sufficiently close to the initial location of another vehicle.
I have now simplified my experiment and run some tests.
The new setup is the following, given a set of jobs
and a set of vehicles
:
I run this experiment 5 times with different amounts of jobs (N_JOBS = [10, 50, 100, 150, 200]
) and fleets of up to 8 vehicles and here are the results.
Highlighted in yellow is you can see the minimum cost needed to solve each VRP (columns). It's notable that for the VRP with 150 Jobs, the solution with 1 vehicle outperforms any other solution with up to 8 vehicles.
Its true that jobs_left was many times empty, unless the jobs were sufficiently close to the initial location of another vehicle
I don't get that: if you have no constraint, any number of vehicles should be able to handle all jobs. If you have no constraint and jobs_left
is not always empty then there is something else screwed in your setup.
It's notable that for the VRP with 150 Jobs, the solution with 1 vehicle outperforms any other solution with up to 8 vehicles.
Hard to draw any conclusion on optimization performance without knowing how your vehicles are defined:
In the latter case, it's not a matter of having a "better" solution, it's simply about using vehicles that are closer to the jobs.
@jonathf no we don't have an ability to serialize the problem to json upstream because usually it's already defined as json in input (that is except for the folks using the C++ libvroom API, kind of a similar situation as the one with pyvroom).
Thanks again @jcoupey for your answers.
I don't get that: if you have no constraint, any number of vehicles should be able to handle all jobs. If you have no constraint and jobs_left is not always empty then there is something else screwed in your setup.
I can confirm that jobs_left
is always empty once I removed the max_travel_time
and capacity
constraints from the vehicles.
If vehicles are geographically distributed, then using more vehicles can lower the cost.
That is the case, I'm creating vehicles at random locations, with the default operating costs (zero fixed cost, only cost per hour). I still believe it is strange is that with a fraction of a fleet I get lower costs than with the whole fleet.
Here is the code for my simplified experiment.
import vroom
import numpy as np
import pandas as pd
import math
from scipy.spatial.distance import cdist
from tqdm import tqdm
import itertools
def generate_random_points( center : tuple, n: int, r: float, seed: int = 42):
"Generates n uniformly distributed random points around a center point within a radius r"
rng = np.random.default_rng(seed=seed)
points = []
cx, cy = center
for _ in range(n):
angle = rng.uniform(0, 2 * math.pi)
distance = rng.uniform(0, r)
dx = distance * math.cos(angle)
dy = distance * math.sin(angle)
new_point = (cx + dx, cy + dy)
points.append(new_point)
return points
def create_random_jobs_and_vehicles(N_JOBS, N_VEHICLES, seed = 42):
center = (0,0)
jobs_loc = generate_random_points(center, N_JOBS, r = 1)
vehicles_loc = generate_random_points(center, N_VEHICLES, r = 0.4)
points = jobs_loc + vehicles_loc
duration_matrix = cdist(points, points, metric='euclidean') * 100
jobs = [vroom.Job(i, location = points.index(loc)) for i, loc in enumerate(jobs_loc)]
vehicles = [vroom.Vehicle(i,
start=points.index(loc),
end=points.index(loc),
)
for i, loc in enumerate(vehicles_loc)]
return jobs, vehicles, duration_matrix
def solve_vrp(jobs, vehicles, matrix):
problem = vroom.Input()
problem.set_durations_matrix(
profile="car",
matrix_input=matrix,
)
problem.add_job(jobs)
problem.add_vehicle(vehicles)
return problem.solve(exploration_level=5, nb_threads=4)
def vrp_with_all_fleet_combinations(jobs, vehicles, matrix):
"""Solves the same VRP for each possible subselection of the vehicles list.
If vehicles = [A, B], it solves the VRP for [A], [B], [A,B]"""
metrics = []
for i in tqdm(range(1, len(vehicles) + 1), desc='Outer Loop'):
for fleet in itertools.combinations(vehicles, i):
id = "_".join([str(v.id) for v in fleet])
solution = solve_vrp(jobs, fleet, matrix)
metrics.append(
{
"id": id,
"n_vehicles": len(fleet),
"n_jobs" : len(jobs),
"cost": solution.summary.cost,
}
)
return metrics
all_metrics = []
N_JOBS_LIST = [10, 50, 100, 150]
max_n_vehicles = 8
for n_jobs in N_JOBS_LIST:
jobs, vehicles, duration_matrix = create_random_jobs_and_vehicles(n_jobs, max_n_vehicles, seed = 42)
print(n_jobs, max_n_vehicles)
metrics = vrp_with_all_fleet_combinations(jobs, vehicles, duration_matrix)
all_metrics.extend(metrics)
df = (pd.DataFrame(all_metrics)
.set_index("id")
.groupby(["n_jobs", "n_vehicles"])
.min()
.reset_index()
.pivot(columns = "n_jobs", values = "cost", index = "n_vehicles")
)
df.style.background_gradient(axis = 0).highlight_min()
Hi @jcoupey @jonathf , I would like to retake this conversation and ask for your input if its ok.
I have crafted a very simple setup in which the unexpected behaviour shows up. I'd love to know a bit more of why this happens, either if it is to understand why my assumptions are wrong or if it helps to identify a possible improvement of vroom.
The setup is the following, I have 5 jobs and 2 vehicles. Vehicles should start and end in the same place. There is no service time, no time windows and no weight constraints.
First I solve the VRP with vehicle 1, then with vehicle 2, and then with both vehicles. The first VRP has a total duration of 388, whereas the others have a duration of 396. I expected the VRP with both vehicles to only use vehicle 1 and thus have also a cost of 388.
import vroom
import numpy as np
duration_matrix = \
np.array( [[ 0., 82., 54., 135., 130., 46., 68.],
[ 82., 0., 95., 134., 91., 73., 50.],
[ 54., 95., 0., 84., 97., 22., 52.],
[135., 134., 84., 0., 63., 90., 88.],
[130., 91., 97., 63., 0., 88., 63.],
[ 46., 73., 22., 90., 88., 0., 33.],
[ 68., 50., 52., 88., 63., 33., 0.]])
jobs = [vroom.Job(i, location = vroom.LocationIndex(i)) for i in range(0,5)]
vehicle_1 = vroom.Vehicle(1, start=5, end=5)
vehicle_2 = vroom.Vehicle(2, start=6, end=6)
def solve_vrp(jobs, vehicles, matrix):
problem = vroom.Input()
problem.set_durations_matrix(
profile="car",
matrix_input=matrix,
)
problem.add_job(jobs)
problem.add_vehicle(vehicles)
return problem.solve(exploration_level=5, nb_threads=4)
solution_vehicle_1 = solve_vrp(jobs, [vehicle_1], duration_matrix)
solution_vehicle_2 = solve_vrp(jobs, [vehicle_2], duration_matrix)
solution_both_vehicles = solve_vrp(jobs, [vehicle_1, vehicle_2], duration_matrix)
print(f"duration with v1 = {solution_vehicle_1.summary.duration}")
print(f"duration with v2 = {solution_vehicle_2.summary.duration}")
print(f"duration with v1 & v2 = {solution_both_vehicles.summary.duration}")
Output
>>>duration with v1 = 388
>>>duration with v2 = 396
>>>duration with v1 & v2 = 396
In the third problem, when vroom has both vehicle_1
and vehicle_2
available to solve the VRP, it decides to assign all jobs to vehicle_2
instead of using vehicle_1
which would yield a more optimal route.
Is there something I am missing?
@juanluisrto really appreciate the efforts to narrow this down to a minimal example! The one you have here looks like it you be pretty straightforward to translate to the upstream json format. Do you think you could post that in a dedicated upstream ticket? This would both ease looking the problem up (at least for me), and allow to close this ticket that is not related to pyvroom per say any more.
I have run an experiment with pyvroom and have obtained some strange results. I'd like to share them here in case someone knows why this happens or in case it opens the door to identifying some bug.
My experiment consists in solving a VRP in two different ways in order to compare their total cost / duration.
jobs
at random locations with random weights and a set ofvehicles
also initialized at random (coordinates are sampled uniformly inside a circle)simple_vrp = VRP(jobs, vehicles)
vehicles_first, vehicles_second
split_first_vrp = VRP(jobs, vehicles_first)
jobs_left = split_first_vrp.unassigned
split_second_vrp = VRP(jobs_left, vehicles_second)
So I am comparing the total cost of the
simple_vrp
vs the sums of the costs ofsplit_first_vrp
andsplit_second_vrp
. The paradox is that sometimescost(simple_vrp) > cost(split_first_vrp) + cost(split_second_vrp)
This is completely counterintuitive to me, since the solver should be able to optimize the fleet usage when knowing all the vehicles available, compared to solving with a subset of the vehicles and then completing the jobs left with the rest of vehicles.To reproduce this behaviour, here is some code:
With this configuration the output of the program is:
There are many other configurations in which this happens (N_JOBS = 100, N_VEHICLES = 3), although it's not the usual case. Do you know why this could be happening? Surely not related to pyvroom per se and its more on the vroom side