From edaf3831686a3c60d71984b1f81621c5ceaff5af Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Thu, 28 Aug 2025 17:26:35 +0200 Subject: [PATCH 1/4] Fix anticipative policy --- .../anticipative_solver.jl | 106 +++++++++++------- src/DynamicVehicleScheduling/plot.jl | 44 +------- src/DynamicVehicleScheduling/state.jl | 54 ++++++++- 3 files changed, 122 insertions(+), 82 deletions(-) diff --git a/src/DynamicVehicleScheduling/anticipative_solver.jl b/src/DynamicVehicleScheduling/anticipative_solver.jl index 863517f..48f3f1c 100644 --- a/src/DynamicVehicleScheduling/anticipative_solver.jl +++ b/src/DynamicVehicleScheduling/anticipative_solver.jl @@ -48,43 +48,57 @@ function anticipative_solver( model_builder=highs_model, two_dimensional_features=env.instance.two_dimensional_features, reset_env=true, - nb_epochs=typemax(Int), + nb_epochs=nothing, seed=get_seed(env), + verbose=false, ) if reset_env reset!(env; reset_rng=true, seed) end + @assert !is_terminated(env) + start_epoch = current_epoch(env) - end_epoch = min(last_epoch(env), start_epoch + nb_epochs - 1) + end_epoch = if isnothing(nb_epochs) + last_epoch(env) + else + min(last_epoch(env), start_epoch + nb_epochs - 1) + end T = start_epoch:end_epoch + TT = (start_epoch + 1):end_epoch # horizon without start epoch + + starting_state = deepcopy(env.state) request_epoch = [0] - for t in T + request_epoch = vcat(request_epoch, fill(start_epoch, customer_count(starting_state))) + for t in TT request_epoch = vcat(request_epoch, fill(t, length(scenario.indices[t]))) end - customer_index = vcat(1, scenario.indices[T]...) - service_time = vcat(0.0, scenario.service_time[T]...) - start_time = vcat(0.0, scenario.start_time[T]...) + + customer_index = vcat(starting_state.location_indices, scenario.indices[TT]...) + service_time = vcat( + starting_state.state_instance.service_time, scenario.service_time[TT]... + ) + start_time = vcat(starting_state.state_instance.start_time, scenario.start_time[TT]...) duration = env.instance.static_instance.duration[customer_index, customer_index] (; epoch_duration, Δ_dispatch) = env.instance model = model_builder() - set_silent(model) + verbose || set_silent(model) nb_nodes = length(customer_index) job_indices = 2:nb_nodes epoch_indices = T - @variable(model, y[i = 1:nb_nodes, j = 1:nb_nodes, t = epoch_indices]; binary=true) + @variable(model, y[i=1:nb_nodes, j=1:nb_nodes, t=epoch_indices]; binary=true) @objective( model, Max, sum( - -duration[i, j] * y[i, j, t] for - i in 1:nb_nodes, j in 1:nb_nodes, t in epoch_indices + -duration[i, j] * y[i, j, t] for i in 1:nb_nodes, j in 1:nb_nodes, + t in epoch_indices ) ) @@ -136,38 +150,55 @@ function anticipative_solver( value.(y), env, customer_index, epoch_indices ) - epoch_indices = Vector{Int}[] - N = 1 - indices = [1] index = 1 - for epoch in 1:last_epoch(env) - M = length(scenario.indices[epoch]) - indices = vcat(indices, (N + 1):(N + M)) - push!(epoch_indices, copy(indices)) + indices = collect(1:(customer_count(starting_state) + 1)) # current known indices in global indexing + epoch_indices = [indices] # store global indices present at each epoch + N = length(indices) # current last index known in global indexing + for epoch in TT # 1:last_epoch(env) + # remove dispatched customers from indices + dispatched = vcat(epoch_routes[index]...) + indices = setdiff(indices, dispatched) + + M = length(scenario.indices[epoch]) # number of new customers in epoch + indices = vcat(indices, (N + 1):(N + M)) # add global indices of customers in epoch + push!(epoch_indices, copy(indices)) # store global indices present at each epoch N = N + M - if epoch in T - dispatched = vcat(epoch_routes[index]...) - index += 1 - indices = setdiff(indices, dispatched) - end + index += 1 end - - indices = vcat(1, scenario.indices...) - start_time = vcat(0.0, scenario.start_time...) - service_time = vcat(0.0, scenario.service_time...) + # epoch_indices = Vector{Int}[] # store global indices present at each epoch + # N = 1 # current last index known in global indexing (= depot) + # index = 1 + # indices = [1] + # for epoch in 1:last_epoch(env) + # M = length(scenario.indices[epoch]) # number of new customers in epoch + # indices = vcat(indices, (N + 1):(N + M)) # add global indices of customers in epoch + # push!(epoch_indices, copy(indices)) + # N = N + M + # if epoch in T # + # dispatched = vcat(epoch_routes[index]...) + # index += 1 + # indices = setdiff(indices, dispatched) + # end + # end + + # indices = vcat(1, scenario.indices...) + # start_time = vcat(0.0, scenario.start_time...) + # service_time = vcat(0.0, scenario.service_time...) dataset = map(enumerate(T)) do (i, epoch) routes = epoch_routes[i] - epoch_customers = epoch_indices[epoch] - - y_true = VSPSolution( - Vector{Int}[ - map(idx -> findfirst(==(idx), epoch_customers), route) for route in routes - ]; - max_index=length(epoch_customers), - ).edge_matrix - - location_indices = indices[epoch_customers] + epoch_customers = epoch_indices[i] + + y_true = + VSPSolution( + Vector{Int}[ + map(idx -> findfirst(==(idx), epoch_customers), route) for + route in routes + ]; + max_index=length(epoch_customers), + ).edge_matrix + + location_indices = customer_index[epoch_customers] new_coordinates = env.instance.static_instance.coordinate[location_indices] new_start_time = start_time[epoch_customers] new_service_time = service_time[epoch_customers] @@ -189,8 +220,7 @@ function anticipative_solver( is_must_dispatch[2:end] .= true else is_must_dispatch[2:end] .= - planning_start_time .+ epoch_duration .+ @view(new_duration[1, 2:end]) .> - new_start_time[2:end] + planning_start_time .+ epoch_duration .+ @view(new_duration[1, 2:end]) .> new_start_time[2:end] end is_postponable[2:end] .= .!is_must_dispatch[2:end] diff --git a/src/DynamicVehicleScheduling/plot.jl b/src/DynamicVehicleScheduling/plot.jl index 98defa2..93343b6 100644 --- a/src/DynamicVehicleScheduling/plot.jl +++ b/src/DynamicVehicleScheduling/plot.jl @@ -192,49 +192,7 @@ Plot a given DVSPState with routes overlaid. This version accepts routes as a Bi where entry (i,j) = true indicates an edge from location i to location j. """ function plot_routes(state::DVSPState, routes::BitMatrix; kwargs...) - # Convert BitMatrix to vector of route vectors - n_locations = size(routes, 1) - route_vectors = Vector{Int}[] - - # Find all outgoing edges from depot (location 1) - depot_destinations = findall(routes[1, :]) - - # For each destination from depot, reconstruct the route - for dest in depot_destinations - if dest != 1 # Skip self-loops at depot - route = Int[] - current = dest - push!(route, current) - - # Follow the route until we return to depot - while true - # Find next location (should be unique for valid routes) - next_locations = findall(routes[current, :]) - - # Filter out the depot for intermediate steps - non_depot_next = filter(x -> x != 1, next_locations) - - if isempty(non_depot_next) - # Must return to depot, route is complete - break - elseif length(non_depot_next) == 1 - # Continue to next location - current = non_depot_next[1] - push!(route, current) - else - # Multiple outgoing edges - this shouldn't happen in valid routes - # but we'll take the first one - current = non_depot_next[1] - push!(route, current) - end - end - - if !isempty(route) - push!(route_vectors, route) - end - end - end - + route_vectors = decode_bitmatrix_to_routes(routes) return plot_routes(state, route_vectors; kwargs...) end diff --git a/src/DynamicVehicleScheduling/state.jl b/src/DynamicVehicleScheduling/state.jl index 0d0a177..415bdc4 100644 --- a/src/DynamicVehicleScheduling/state.jl +++ b/src/DynamicVehicleScheduling/state.jl @@ -149,7 +149,8 @@ function is_feasible(state::DVSPState, routes::Vector{Vector{Int}}; verbose::Boo if all(is_dispatched[is_must_dispatch]) return true else - verbose && @warn "Not all must-dispatch requests are dispatched" + verbose && + @warn "Not all must-dispatch requests are dispatched $(is_dispatched[is_must_dispatch])" return false end end @@ -180,6 +181,57 @@ function apply_routes!( return c end +function decode_bitmatrix_to_routes(routes::BitMatrix) + # Convert BitMatrix to vector of route vectors + n_locations = size(routes, 1) + route_vectors = Vector{Int}[] + + # Find all outgoing edges from depot (location 1) + depot_destinations = findall(routes[1, :]) + + # For each destination from depot, reconstruct the route + for dest in depot_destinations + if dest != 1 # Skip self-loops at depot + route = Int[] + current = dest + push!(route, current) + + # Follow the route until we return to depot + while true + # Find next location (should be unique for valid routes) + next_locations = findall(routes[current, :]) + + # Filter out the depot for intermediate steps + non_depot_next = filter(x -> x != 1, next_locations) + + if isempty(non_depot_next) + # Must return to depot, route is complete + break + elseif length(non_depot_next) == 1 + # Continue to next location + current = non_depot_next[1] + push!(route, current) + else + # Multiple outgoing edges - this shouldn't happen in valid routes + # but we'll take the first one + current = non_depot_next[1] + push!(route, current) + end + end + + if !isempty(route) + push!(route_vectors, route) + end + end + end + return route_vectors +end + +function apply_routes!(state::DVSPState, routes::BitMatrix; check_feasibility::Bool=true) + route_vectors = decode_bitmatrix_to_routes(routes) + return apply_routes!(state, route_vectors; check_feasibility) +end + function cost(state::DVSPState, routes::Vector{Vector{Int}}) return cost(routes, duration(state.state_instance)) end From 39cac5a3130d5bbe16a19dc952b5b5207e7e36ad Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Tue, 2 Sep 2025 11:01:06 +0200 Subject: [PATCH 2/4] bugfix --- .../DynamicVehicleScheduling.jl | 5 ++++- .../anticipative_solver.jl | 22 ++----------------- src/Utils/interface.jl | 2 +- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/DynamicVehicleScheduling/DynamicVehicleScheduling.jl b/src/DynamicVehicleScheduling/DynamicVehicleScheduling.jl index 6b6c5b2..38bd82a 100644 --- a/src/DynamicVehicleScheduling/DynamicVehicleScheduling.jl +++ b/src/DynamicVehicleScheduling/DynamicVehicleScheduling.jl @@ -111,7 +111,10 @@ function Utils.generate_policies(b::DynamicVehicleSchedulingBenchmark) return (lazy, greedy) end -function Utils.generate_statistical_model(b::DynamicVehicleSchedulingBenchmark) +function Utils.generate_statistical_model( + b::DynamicVehicleSchedulingBenchmark; seed=nothing +) + Random.seed!(seed) return Chain(Dense((b.two_dimensional_features ? 2 : 14) => 1), vec) end diff --git a/src/DynamicVehicleScheduling/anticipative_solver.jl b/src/DynamicVehicleScheduling/anticipative_solver.jl index 48f3f1c..26aac3b 100644 --- a/src/DynamicVehicleScheduling/anticipative_solver.jl +++ b/src/DynamicVehicleScheduling/anticipative_solver.jl @@ -165,25 +165,6 @@ function anticipative_solver( N = N + M index += 1 end - # epoch_indices = Vector{Int}[] # store global indices present at each epoch - # N = 1 # current last index known in global indexing (= depot) - # index = 1 - # indices = [1] - # for epoch in 1:last_epoch(env) - # M = length(scenario.indices[epoch]) # number of new customers in epoch - # indices = vcat(indices, (N + 1):(N + M)) # add global indices of customers in epoch - # push!(epoch_indices, copy(indices)) - # N = N + M - # if epoch in T # - # dispatched = vcat(epoch_routes[index]...) - # index += 1 - # indices = setdiff(indices, dispatched) - # end - # end - - # indices = vcat(1, scenario.indices...) - # start_time = vcat(0.0, scenario.start_time...) - # service_time = vcat(0.0, scenario.service_time...) dataset = map(enumerate(T)) do (i, epoch) routes = epoch_routes[i] @@ -215,7 +196,7 @@ function anticipative_solver( epoch_duration = env.instance.epoch_duration Δ_dispatch = env.instance.Δ_dispatch planning_start_time = (epoch - 1) * epoch_duration + Δ_dispatch - if epoch == last_epoch + if epoch == end_epoch # If we are in the last epoch, all requests must be dispatched is_must_dispatch[2:end] .= true else @@ -223,6 +204,7 @@ function anticipative_solver( planning_start_time .+ epoch_duration .+ @view(new_duration[1, 2:end]) .> new_start_time[2:end] end is_postponable[2:end] .= .!is_must_dispatch[2:end] + # TODO: avoid code duplication with add_new_customers! state = DVSPState(; state_instance=static_instance, diff --git a/src/Utils/interface.jl b/src/Utils/interface.jl index 1fa1f65..1aabda6 100644 --- a/src/Utils/interface.jl +++ b/src/Utils/interface.jl @@ -232,7 +232,7 @@ Generate a vector of environments for the given dynamic benchmark and dataset. """ function generate_environments( bench::AbstractDynamicBenchmark, - dataset::Vector{<:DataSample}; + dataset::AbstractArray{<:DataSample}; seed=nothing, rng=MersenneTwister(seed), kwargs..., From 338f990e740345d2613eddcc6e19c1014375c5cd Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Tue, 2 Sep 2025 11:15:54 +0200 Subject: [PATCH 3/4] formatter --- .../anticipative_solver.jl | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/DynamicVehicleScheduling/anticipative_solver.jl b/src/DynamicVehicleScheduling/anticipative_solver.jl index 26aac3b..b2e2452 100644 --- a/src/DynamicVehicleScheduling/anticipative_solver.jl +++ b/src/DynamicVehicleScheduling/anticipative_solver.jl @@ -91,14 +91,14 @@ function anticipative_solver( job_indices = 2:nb_nodes epoch_indices = T - @variable(model, y[i=1:nb_nodes, j=1:nb_nodes, t=epoch_indices]; binary=true) + @variable(model, y[i = 1:nb_nodes, j = 1:nb_nodes, t = epoch_indices]; binary=true) @objective( model, Max, sum( - -duration[i, j] * y[i, j, t] for i in 1:nb_nodes, j in 1:nb_nodes, - t in epoch_indices + -duration[i, j] * y[i, j, t] for + i in 1:nb_nodes, j in 1:nb_nodes, t in epoch_indices ) ) @@ -170,14 +170,12 @@ function anticipative_solver( routes = epoch_routes[i] epoch_customers = epoch_indices[i] - y_true = - VSPSolution( - Vector{Int}[ - map(idx -> findfirst(==(idx), epoch_customers), route) for - route in routes - ]; - max_index=length(epoch_customers), - ).edge_matrix + y_true = VSPSolution( + Vector{Int}[ + map(idx -> findfirst(==(idx), epoch_customers), route) for route in routes + ]; + max_index=length(epoch_customers), + ).edge_matrix location_indices = customer_index[epoch_customers] new_coordinates = env.instance.static_instance.coordinate[location_indices] @@ -201,7 +199,8 @@ function anticipative_solver( is_must_dispatch[2:end] .= true else is_must_dispatch[2:end] .= - planning_start_time .+ epoch_duration .+ @view(new_duration[1, 2:end]) .> new_start_time[2:end] + planning_start_time .+ epoch_duration .+ @view(new_duration[1, 2:end]) .> + new_start_time[2:end] end is_postponable[2:end] .= .!is_must_dispatch[2:end] # TODO: avoid code duplication with add_new_customers! From 3fe00279d1e624b76ce221ed145d6e3d2e8f70ec Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Tue, 2 Sep 2025 13:30:14 +0200 Subject: [PATCH 4/4] better tests --- docs/make.jl | 2 +- src/DynamicVehicleScheduling/state.jl | 9 +++++---- test/dynamic_vsp.jl | 5 +++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index b4eaaf9..1d37ff9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,7 +13,7 @@ tutorial_files = readdir(tutorial_dir) md_tutorial_files = [split(file, ".")[1] * ".md" for file in tutorial_files] benchmark_files = [joinpath("benchmarks", e) for e in readdir(benchmarks_dir)] -include_tutorial = false +include_tutorial = true if include_tutorial for file in tutorial_files diff --git a/src/DynamicVehicleScheduling/state.jl b/src/DynamicVehicleScheduling/state.jl index 415bdc4..5b6e37d 100644 --- a/src/DynamicVehicleScheduling/state.jl +++ b/src/DynamicVehicleScheduling/state.jl @@ -212,10 +212,11 @@ function decode_bitmatrix_to_routes(routes::BitMatrix) current = non_depot_next[1] push!(route, current) else - # Multiple outgoing edges - this shouldn't happen in valid routes - # but we'll take the first one - current = non_depot_next[1] - push!(route, current) + throw( + ErrorException( + "Invalid route: multiple outgoing edges from location $current" + ), + ) end end diff --git a/test/dynamic_vsp.jl b/test/dynamic_vsp.jl index 0f890c0..c2ea1f3 100644 --- a/test/dynamic_vsp.jl +++ b/test/dynamic_vsp.jl @@ -46,4 +46,9 @@ y2 = maximizer(θ2; instance=instance2) @test size(x, 1) == 2 @test size(x2, 1) == 14 + + anticipative_value, solution = generate_anticipative_solution(b, env; reset_env=true) + reset!(env; reset_rng=true) + cost = sum(step!(env, sample.y_true) for sample in solution) + @test isapprox(cost, anticipative_value; atol=1e-5) end