Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AddDisjunction function does not work like it supposed #1348

Open
teemusanteri opened this issue Jun 11, 2019 · 7 comments
Open

AddDisjunction function does not work like it supposed #1348

teemusanteri opened this issue Jun 11, 2019 · 7 comments
Assignees
Labels
Doc: Optimization Site Issue related to https://developers.google.com/optimization/ or Documentation in general Help Needed Modeling/Usage problem Solver: Routing Uses the Routing library and the original CP solver
Projects
Milestone

Comments

@teemusanteri
Copy link

Hi,

Adddisjunction does not work like it is documented.

See the forum topic:

https://groups.google.com/forum/m/?utm_medium=email&utm_source=footer#!msg/or-tools-discuss/703V05No26s/oQbpsPUCAgAJ

@jmarca
Copy link
Contributor

jmarca commented Jun 11, 2019

To repeat the forum posts here a little bit.

The disjunctions by themselves aren't the problem. The problem comes when there is some sort of binary flip flop between one state and another. Here we want to choose one of two vehicles. The idea is a truck by itself, or a truck with a trailer---obviously you can't drive both.

This is accomplished by the lines:

    for veh_pair in range(0, num_veh//2):
        combo = veh_pair*2
        single = veh_pair*2 + 1
        index_combo = routing.End(combo)
        index_single = routing.End(single)
        
        end_time_combo = time_dimension.CumulVar(index_combo)
        end_time_single = time_dimension.CumulVar(index_single)
        
        combo_on = end_time_combo > 0
        single_on = end_time_single > 0
        solver.Add(combo_on * single_on == 0)

This works, but only without disjunctions. So if the problem has more destinations than can be served by the vehicles, you need disjunctions to allow dropping of nodes. However, once you turn on disjunctions, the solver fails to find the correct solution.

Attached code---disjunction_fail.zip---demonstrates the problem. See also my repo at https://github.com/jmarca/disjunction_tests.

Running without disjunctions (the default) and with just 5 destinations, the solver does the right thing---uses two combo trucks, one with three visits, the other with two. Logging turned on to show the solver's progress.

python disjunction_fail.py    -l 1
WARNING: Logging before InitGoogleLogging() is written to STDERR
I0611 19:59:20.584077     8 search.cc:252] Start search (memory used = 42.73 MB)
I0611 19:59:20.584228     8 search.cc:252] Root node processed (time = 0 ms, constraints = 110, memory used = 42.73 MB)
I0611 19:59:20.584597     8 search.cc:252] Solution #0 (75, time = 0 ms, branches = 52, failures = 8, depth = 33, memory used = 42.73 MB)
I0611 19:59:20.611366     8 search.cc:252] Finished search tree (time = 27 ms, branches = 2599, failures = 1518, neighbors = 338, filtered neighbors = 133, accepted neigbors = 0, memory used = 42.73 MB)
I0611 19:59:20.611438     8 search.cc:252] End search (time = 27 ms, branches = 2599, failures = 1518, memory used = 42.73 MB, speed = 96259 branches/s)
The Objective Value is 75
Truck 0 travel time
     combo: 40 
     single: 0
Truck 1 travel time
     combo: 35 
     single: 0
(etc)

Run with disjunctions turned on, again with just 5 demand nodes, the solver fails. Instead of assigning all five as before, it decides to drop one node, no matter how large the penalty is.

 python disjunction_fail.py  -d 1 --singlepenalty 500  -l 1
single node disjunction penalty is 500
added 5 disjunctions, one per node
WARNING: Logging before InitGoogleLogging() is written to STDERR
I0611 20:01:10.471397     9 routing.cc:2833] All Unperformed Solution (2500, time = 0 ms, memory used = 42.73 MB)
I0611 20:01:10.471446     9 search.cc:252] Start search (memory used = 42.73 MB)
I0611 20:01:10.471575     9 search.cc:252] Root node processed (time = 0 ms, constraints = 125, memory used = 42.73 MB)
I0611 20:01:10.471832     9 search.cc:252] Solution #0 (2030, time = 0 ms, branches = 38, failures = 0, depth = 33, memory used = 42.77 MB)
I0611 20:01:10.472105     9 search.cc:252] Solution #1 (2006, objective maximum = 2030, time = 0 ms, branches = 41, failures = 2, depth = 33, Relocate<1>, neighbors = 1, filtered neighbors = 1, accepted neighbors = 1, memory used = 42.77 MB)
I0611 20:01:10.472709     9 search.cc:252] Solution #2 (1536, objective maximum = 2030, time = 1 ms, branches = 45, failures = 5, depth = 33, MakeActiveOperator, neighbors = 18, filtered neighbors = 3, accepted neighbors = 2, memory used = 42.77 MB)
I0611 20:01:10.472937     9 search.cc:252] Solution #3 (1041, objective maximum = 2030, time = 1 ms, branches = 48, failures = 7, depth = 33, MakeActiveOperator, neighbors = 19, filtered neighbors = 4, accepted neighbors = 3, memory used = 42.77 MB)
I0611 20:01:10.473183     9 search.cc:252] Solution #4 (546, objective maximum = 2030, time = 1 ms, branches = 53, failures = 9, depth = 33, MakeActiveOperator, neighbors = 20, filtered neighbors = 5, accepted neighbors = 4, memory used = 42.77 MB)
I0611 20:01:10.487522     9 search.cc:252] Finished search tree (time = 16 ms, branches = 827, failures = 622, neighbors = 300, filtered neighbors = 147, accepted neigbors = 4, memory used = 42.77 MB)
I0611 20:01:10.487587     9 search.cc:252] End search (time = 16 ms, branches = 827, failures = 622, memory used = 42.77 MB, speed = 51687 branches/s)
The Objective Value is 546
Truck 0 travel time
     combo: 0 
     single: 6
Truck 1 travel time
     combo: 40 
     single: 0
Route for vehicle 0:
 0 Load(0) Cost(0) ->  0 Load(0) Cost(0)
Distance of the route: 0m
Load of the route: 0
(etc)
@jmarca
Copy link
Contributor

jmarca commented Jun 13, 2019

Any thoughts or comments on this? Is there something more I can do to clarify the issue?

@jmarca
Copy link
Contributor

jmarca commented Jun 18, 2019

I can't speak for the original poster's bug, but my confirmation of it seems to be fixed.

My possible solution is that I correctly added the "guided_local_search" flag, and it seems to be working now.

Updated the python in my repo: https://github.com/jmarca/disjunction_tests/blob/master/disjunction_fail.py

I have not tested all of the failing cases I found, but so far things seem to work.

The fix:

Previously I was using

    search_parameters.first_solution_strategy = (
        routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)

Which is nonsense, but passes for some reason (I think because GUIDED_LOCAL_SEARCH is just 2, which translates to a solution strategy that makes sense but certainly not the local search metaheuristic of guided local search...

So, fixing that,

    search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.GLOBAL_CHEAPEST_ARC) 
    search_parameters.local_search_metaheuristic = (
        routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
    search_parameters.time_limit.seconds =  60  # timelimit

And then the proper solution pops out

@lperron lperron added Help Needed Modeling/Usage problem Solver: Routing Uses the Routing library and the original CP solver labels Jun 19, 2019
@lperron lperron closed this as completed Jun 19, 2019
@Mizux Mizux self-assigned this Oct 26, 2020
@Mizux Mizux added this to the v8.0 milestone Feb 12, 2021
@kevindalmeijer
Copy link

Would it be possible to reopen this issue to update the documentation? I agree with @teemusanteri that the behavior of AddDisjunction() does not match the documentation when len(indices) > 1. The Python documentation and the C++ documentation both state:

This is equivalent to adding the constraint: p + Sum(i)active[i] == max_cardinality where p is an integer variable, and the following cost to the cost function: p * penalty.

The code below sets up a small CVRP with four customers and one vehicle of capacity 2. When I add a disjunction on [1, 2, 3, 4] with penalty=1 and max_capacity=4. I get the route 0 Load(0) -> 4 Load(1) -> 3 Load(2) -> 0 Load(2) with penalty 1. However, based on the documentation I expected to see a penalty of 2, because max_cardinality - Sum(i)active[i] = 2.

To be clear, I do not suggest to change this behavior, as the functionality is great. However, it would be helpful to update the documentation to avoid confusion.

Example code tested on ortools 9.10.4067 (simplified version of the CVRP example):

"""Capacited Vehicles Routing Problem (CVRP)."""

from ortools.constraint_solver import pywrapcp


def create_data_model():
    """Stores the data for the problem."""
    data = {}
    data["distance_matrix"] = [
        # fmt: off
      [0, 548, 776, 696, 582],
      [548, 0, 684, 308, 194],
      [776, 684, 0, 992, 878],
      [696, 308, 992, 0, 114],
      [582, 194, 878, 114, 0],
        # fmt: on
    ]
    data["demands"] = [0, 1, 1, 1, 1]
    data["vehicle_capacities"] = [2]
    data["num_vehicles"] = 1
    data["depot"] = 0
    return data


def print_solution(data, manager, routing, solution):
    """Prints solution on console."""
    print(f"Objective: {solution.ObjectiveValue()}")
    for vehicle_id in range(data["num_vehicles"]):
        index = routing.Start(vehicle_id)
        plan_output = f"Route for vehicle {vehicle_id}:\n"
        route_distance = 0
        route_load = 0
        while not routing.IsEnd(index):
            node_index = manager.IndexToNode(index)
            route_load += data["demands"][node_index]
            plan_output += f" {node_index} Load({route_load}) -> "
            previous_index = index
            index = solution.Value(routing.NextVar(index))
            route_distance += routing.GetArcCostForVehicle(
                previous_index, index, vehicle_id
            )
        plan_output += f" {manager.IndexToNode(index)} Load({route_load})\n"
        print(plan_output)


def main():
    """Solve the CVRP problem."""
    # Instantiate the data problem.
    data = create_data_model()

    # Create the routing index manager.
    manager = pywrapcp.RoutingIndexManager(
        len(data["distance_matrix"]), data["num_vehicles"], data["depot"]
    )

    # Create Routing Model.
    routing = pywrapcp.RoutingModel(manager)

    # Add Capacity constraint.
    def demand_callback(from_index):
        """Returns the demand of the node."""
        # Convert from routing variable Index to demands NodeIndex.
        from_node = manager.IndexToNode(from_index)
        return data["demands"][from_node]

    demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
    routing.AddDimensionWithVehicleCapacity(
        demand_callback_index,
        0,  # null capacity slack
        data["vehicle_capacities"],  # vehicle maximum capacities
        True,  # start cumul to zero
        "Capacity",
    )

    ### RELEVANT CODE ###
    # add disjunction for all nodes with penalty 1
    penalty = 1
    max_cardinality = 4
    routing.AddDisjunction([manager.NodeToIndex(node) for node in range(1, 5)], penalty, max_cardinality)
    ### RELEVANT CODE ###

    # Setting first solution heuristic.
    search_parameters = pywrapcp.DefaultRoutingSearchParameters()
    search_parameters.time_limit.FromSeconds(1)

    # Solve the problem.
    solution = routing.SolveWithParameters(search_parameters)

    # Print solution on console.
    if solution:
        print_solution(data, manager, routing, solution)
        print("Total penalty: {}".format(solution.ObjectiveValue()))

if __name__ == "__main__":
    main()
@jmarca
Copy link
Contributor

jmarca commented May 22, 2024 via email

@Mizux Mizux reopened this May 23, 2024
@Mizux Mizux modified the milestones: v8.0, v10.0 May 23, 2024
@Mizux Mizux added the Doc: Optimization Site Issue related to https://developers.google.com/optimization/ or Documentation in general label May 23, 2024
@Mizux Mizux added this to To do in ToDo via automation May 23, 2024
@Mizux
Copy link
Collaborator

Mizux commented May 23, 2024

source of the ref doc:

/// Adds a disjunction constraint on the indices: exactly 'max_cardinality' of
/// the indices are active. Start and end indices of any vehicle cannot be
/// part of a disjunction.
///
/// If a penalty is given, at most 'max_cardinality' of the indices can be
/// active, and if less are active, 'penalty' is payed per inactive index.
/// This is equivalent to adding the constraint:
/// p + Sum(i)active[i] == max_cardinality
/// where p is an integer variable, and the following cost to the cost
/// function:
/// p * penalty.
/// 'penalty' must be positive to make the disjunction optional; a negative
/// penalty will force 'max_cardinality' indices of the disjunction to be
/// performed, and therefore p == 0.
/// Note: passing a vector with a single index will model an optional index
/// with a penalty cost if it is not visited.

and just to get syntax coloring of jmarca post (not working on email reply)...

const int64_t max_cardinality =
disjunctions_[disjunction].value.max_cardinality;
IntVar* no_active_var = solver_->MakeBoolVar();
IntVar* number_active_vars = solver_->MakeIntVar(0, max_cardinality);
solver_->AddConstraint(
solver_->MakeSumEquality(disjunction_vars, number_active_vars));
solver_->AddConstraint(solver_->MakeIsDifferentCstCt(
number_active_vars, max_cardinality, no_active_var));
const int64_t penalty = disjunctions_[disjunction].value.penalty;
if (penalty < 0) {
no_active_var->SetMax(0);
return nullptr;
} else {
return solver_->MakeProd(no_active_var, penalty)->Var();
}

@jmarca
Copy link
Contributor

jmarca commented May 23, 2024

(I never noticed email replies do not support markdown. I'll keep that in mind in the future)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Doc: Optimization Site Issue related to https://developers.google.com/optimization/ or Documentation in general Help Needed Modeling/Usage problem Solver: Routing Uses the Routing library and the original CP solver
5 participants