So I've been hacking at this and I had a small bit of success but then
ran into more failure.
Working from Mizux's gist version, I modified the code to replicate
the issue surfaced by Angelo.
My modified gist is here:
https://gist.github.com/jmarca/c78b0b5d785e36cf89f15692d68165b0
I've been hacking on this and have forgotten what works, what doesn't,
but definitely it isn't working.
run with python chain_modified_with_disjunctions.py -h
The "time windows" are actually distance windows
data['distance_windows'] = {
2: [0, 1000],
5: [0, 1200],
6: [600, 1200],
# 8: [600, 1200],
13: [600, 1200], # try 11, 12, etc, solver breaks
# 16: [600, 1200], # if uncommented, solver cannot use 11 to
# 16, so does the right thing (keeps 6, drops 11 to 15)
}
Unhelpful as far as solving the problem goes, but perhaps it is a
solver bug.
James
On Thu, Mar 04, 2021 at 05:44:25AM -0800, angelo manzatto wrote:
> Excellent! It worked, but now I am dealing with the first big challenge
> that was conciliate the route constraint with drop penalty to infeasible
> nodes.
>
> *Event *nodes have higher priority over *route *nodes so the penalty
> applied on them is 1000 time more but the solver prefers paying the cost
> for some unknown reason.
> Attached again I put a toy sample adding the constraint code above and time
> windows with the following parameters:
>
> data['starts'] = 1 # vehicle starting node
> data['ends'] = 0 # vehicle ending node
> data['events'] = [2] # events
> data['routes'] = [[3,4,5]] # routes
>
> route node penalty = 1000
> event node penalty = 1000000
>
> Time windows are made in such a way that or you do the route or the event,
> but even with event having a higher drop penalty the solver is doing the
> route way:
> *Expected solution*: [2] (Objective Cost: *30684 , *dropped nodes: [3,4,5])
> *Solver solution: *[3,4,5] (Objective Cost: *1000000958, *dropped nodes:
> >> 1 -> *4 -> 3* -> 11 -> 12 -> 13 -> 14 -> 15 -> 16 -> 8 -> 9
> >> -> 10 -> *2* -> 6 -> *5* -> 7 -> 0
> >> Distance of the route: 5374m
> >>
> >> Maximum of the route distances: 5374m
> >> ```
> >> Le jeudi 4 mars 2021 à 08:57:45 UTC+1, Mizux Seiha a écrit :
> >>
> >>> My 2 cents
> >>>
> >>> 1. Since you can insert event inside a "node chain" e.g. `6 -> 7 -> 2
> >>> -> 8`
> >>> I won't try to use IntVar* ApplyLocks(const std::vector<int64>& locks);
> >>> ref:
> >>>
https://github.com/google/or-tools/blob/b77bd3ac69b7f3bb02f55b7bab6cbb4bab3917f2/ortools/constraint_solver/routing.h#L1031-L1039
> >>>
> >>> 2. So for a chain I would use a counter and some constraint
> >>> ```python
> >>> six = manager.NodeToIndex(6)
> >>> seven = manager.NodeToIndex(7)
> >>> routing.solver().Add(counter_dim.CumulVar(six)
> >>> < counter_dim.CumulVar(seven))
> >>> ```
> >>>
> >>> 3. between chain [6,7,8] and [9,10,11] basically you just want `8 < 9`
> >>> *OR* `11 < 6` depending on whether the vehicle will visits the first
> >>> chain or the second one
> >>>
> >>> Le mercredi 3 mars 2021 à 20:42:37 UTC+1,
angelo....@gmail.com a écrit :
> >>>
> >>>> Hi, I am having difficult to model a route sequence constraint with
> >>>> optional nodes called "events" in the middle. So my data dict has a 'route'
> >>>> key with a list of routes that must be followed in sequence like [1,2,3] in
> >>>> that the vehicle MUST visit 1 -> 2 -> 3 and a 'event' key with optional
> >>>> nodes that can be inserted anywhere in the middle or even outside of the
> >>>> route but with the condition that the you cannot move to another route
> >>>> before completing the route you are already in.
> >>>>
> >>>> Example:
> >>>> data['starts'] = 1 # vehicle starting node
> >>>> data['ends'] = 0 # vehicle ending node
> >>>> data['routes'] = [[6,7,8], [9,10,11]] # routes
> >>>> data['events'] = [2,3,4,5] # events
> >>>>
> >>>> You could go for [1,6,7,2,3,8,4,9,10,5,11,0]
> >>>> You can't go for [1,*6*,*7*,2,3,9,10,5,11,*8*,4,0] since you didn't
> >>>> finish route [6,7,8] before moving to route [9,10,11]
> >>>>
> >>>> My strategy on modeling this constraint are:
> >>>> * same vehicle for all nodes
> >>>> * removing all non next nodes from nodes inside same route
> >>>> * removing all non start nodes from other routes when in end node
> >>>> inside route
> >>>> * using count dimension
> >>>>
> >>>> Here is a toy sample for testing but it is falling to achieve the
> >>>> "finish entire route" before moving to another because I don't know how to
> >>>> guarantee that after going to an event node you MUST return to the route
> >>>> you already started. If you run the example the result would be: (1 -> 4
> >>>> -> 11 -> 12 -> 13 -> 14 -> 5 -> 8 -> 9 -> 10 -> 2 -> 6 -> 7
> >>>> -> 3 -> 15 -> 16 -> 0) but it is incorrect since you didn't finish
> >>>> the route [11,12,13,14,15,16] before moving to other routes.
> >>>>
> >>>> Also I will have to add an additional drop disjunction in favor of
> >>>> 'event's over 'routes' in the future meaning maybe more one challenge.
> >>>>
> >>>>
>
> --
> You received this message because you are subscribed to the Google Groups "or-tools-discuss" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to
or-tools-discu...@googlegroups.com.
> To view this discussion on the web visit
https://groups.google.com/d/msgid/or-tools-discuss/a5855372-d264-4878-9f57-d2c807d6bc7an%40googlegroups.com.
> # -*- coding: utf-8 -*-
> """
> Created on Wed Mar 3 14:06:28 2021
>
> @author: sirga
> """
>
> ##################################################################################################
> # Libraries
> ##################################################################################################
>
> from ortools.constraint_solver import routing_enums_pb2
> from ortools.constraint_solver import pywrapcp
>
> ##################################################################################################
> # Print Function
> ##################################################################################################
> def print_solution(data, manager, routing, solution):
> """Prints solution on console."""
> max_route_distance = 0
> for vehicle_id in range(data['num_vehicles']):
> index = routing.Start(vehicle_id)
> plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
> route_distance = 0
> while not routing.IsEnd(index):
> plan_output += ' {} -> '.format(manager.IndexToNode(index))
>
> previous_index = index
> index = solution.Value(routing.NextVar(index))
> route_distance += routing.GetArcCostForVehicle(
> previous_index, index, vehicle_id)
> plan_output += '{}\n'.format(manager.IndexToNode(index))
> plan_output += 'Distance of the route: {}m\n'.format(route_distance)
> print(plan_output)
> max_route_distance = max(route_distance, max_route_distance)
> print('Maximum of the route distances: {}m'.format(max_route_distance))
>
> ##################################################################################################
> # Parameters
> ##################################################################################################
>
> data = {}
> data['distance_matrix'] = [
> [ 0, 0, 0, 0, 0, 0],
> [ 0, 0, 684, 308, 194, 502 ],
> [ 0, 684, 0, 992, 878, 502 ],
> [ 0, 308, 992, 0, 114, 650 ],
> [ 0, 194, 878, 114, 0, 536 ],
> [ 0, 502, 502, 650, 536, 0 ],
> ]
>
> data['time_matrix'] = [
> [0, 6, 9, 8, 7, 3],
> [6, 0, 8, 3, 2, 6],
> [9, 8, 0, 11, 10, 6],
> [8, 3, 11, 0, 1, 7],
> [7, 2, 10, 1, 0, 6],
> [3, 6, 6, 7, 6, 0]
> ]
>
> data['time_windows'] = [
> (0, 999), # depot
> (0, 999), # 1
> (10, 10), # 2
> (16, 16), # 3
> (32, 32), # 4
> (55, 55) # 5
> ]#5
>
> data['num_vehicles'] = 1
> data['starts'] = [1]
> data['ends'] = [0]
>
> data['events'] = [2]
> data['routes'] = [[3, 4, 5]]
>
> ##################################################################################################
> # Model
> ##################################################################################################
>
> # Create the routing index manager.
> manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
> data['num_vehicles'],
> data['starts'],
> data['ends'])
>
> # Create Routing Model.
> routing = pywrapcp.RoutingModel(manager)
>
> ##################################################################################################
> # Dimensions
> ##################################################################################################
> # Create and register a transit callback.
> def distance_callback(from_index, to_index):
> """Returns the distance between the two nodes."""
> # Convert from routing variable Index to distance matrix NodeIndex.
> from_node = manager.IndexToNode(from_index)
> to_node = manager.IndexToNode(to_index)
> return data['distance_matrix'][from_node][to_node]
>
> transit_callback_index = routing.RegisterTransitCallback(distance_callback)
>
> # Define cost of each arc.
> routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
>
> # Add Distance constraint.
> dimension_name = 'Distance'
> routing.AddDimension(
> transit_callback_index,
> 0, # no slack
> 99999999999, # vehicle maximum travel distance
> True, # start cumul to zero
> dimension_name)
> distance_dimension = routing.GetDimensionOrDie(dimension_name)
> distance_dimension.SetGlobalSpanCostCoefficient(0)
>
> #####################
> # Count dimensions
> #####################
> routing.AddConstantDimension(
> 1,
> 1000000,
> True,
> "Count")
> count_dimension = routing.GetDimensionOrDie("Count")
>
> #####################
> # Add Time dimension
> #####################
> def time_callback(from_index, to_index):
> """Returns the travel time between the two nodes."""
> # Convert from routing variable Index to time matrix NodeIndex.
> from_node = manager.IndexToNode(from_index)
> to_node = manager.IndexToNode(to_index)
> return data['time_matrix'][from_node][to_node]
>
> transit_callback_index = routing.RegisterTransitCallback(time_callback)
>
> time = 'Time'
> routing.AddDimension(
> transit_callback_index,
> 1000000000000, # allow waiting time
> 1000000000000, # maximum time per vehicle
> False, # Don't force start cumul to zero.
> time)
> time_dimension = routing.GetDimensionOrDie(time)
> # Add time window constraints for each location except depot.
> for location_idx, time_window in enumerate(data['time_windows']):
> if location_idx == 0:
> continue
> index = manager.NodeToIndex(location_idx)
> time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1])
>
> for i in range(data['num_vehicles']):
> routing.AddVariableMinimizedByFinalizer(
> time_dimension.CumulVar(routing.Start(i)))
> routing.AddVariableMinimizedByFinalizer(
> time_dimension.CumulVar(routing.End(i)))
>
> ##################################################################################################
> # Parameters
> ##################################################################################################
> # Setting first solution heuristic.
> search_parameters = pywrapcp.DefaultRoutingSearchParameters()
>
> search_parameters.first_solution_strategy = (
> routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
>
> search_parameters.local_search_metaheuristic = (
> routing_enums_pb2.LocalSearchMetaheuristic.AUTOMATIC)
> search_parameters.log_search = True
>
> # Search time limit to find a solution
> search_parameters.time_limit.seconds = 60
>
> ##################################################################################################
> # Sequence constraint
> ##################################################################################################
> for route in data['routes']:
> for i in range(len(route) - 1):
> current_index = manager.NodeToIndex(route[i])
> next_index = manager.NodeToIndex(route[i + 1])
> routing.solver().Add(
> count_dimension.CumulVar(current_index) <
> count_dimension.CumulVar(next_index))
>
> # Enforce same vehicle on route sequence
> # note(mizux): note needed for a single vehicle problem...
> for route in data['routes']:
> for i in range(len(route) - 1):
> current_index = manager.NodeToIndex(route[i])
> next_index = manager.NodeToIndex(route[i + 1])
> # same vehicle condition
> routing.solver().Add(
> routing.VehicleVar(current_index) == routing.VehicleVar(next_index))
>
> # Forbid interleaved route chains
> # i.e. we have for any pair of chain X, Y "end_X < start_Y OR end_Y < start_X"
> for i in range(len(data['routes'])):
> for j in range(i+1, len(data['routes'])):
> #print(f'indices {i},{j}')
> start_i = manager.NodeToIndex(data['routes'][i][0])
> end_i = manager.NodeToIndex(data['routes'][i][-1])
> start_j = manager.NodeToIndex(data['routes'][j][0])
> end_j = manager.NodeToIndex(data['routes'][j][-1])
> #print(f'{end_i} < {start_j}')
> #print(f'{end_j} < {start_i}')
> first = count_dimension.CumulVar(end_i) < count_dimension.CumulVar(start_j)
> second = count_dimension.CumulVar(end_j) < count_dimension.CumulVar(start_i)
> routing.solver().Add(routing.solver().Sum([first, second]) == 1)
>
> #################################################################
> # Add disjunction
> #################################################################
>
> # Add penalty to route nodes
> for route in data['routes']:
> for i in range(len(route)):
> node_index = manager.NodeToIndex(route[i])
> routing.AddDisjunction([node_index], 10000)
>
> # Add penalty for event. Event penalty should be higher than route penalty to avoid drop it
> for event_node in data['events']:
> event_index = manager.NodeToIndex(event_node)
> routing.AddDisjunction([event_index], 1000000000)
>
> ##################################################################################################
> # Solve problem
> ##################################################################################################
> solution = routing.SolveWithParameters(search_parameters)
>
> if solution:
> print_solution(data, manager, routing, solution)
> print(f"Objective Cost: {solution.ObjectiveValue()}")
> else:
> print("NO SOLUTION FOUND!")
--
James E. Marca
Activimetrics LLC