Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/DynamicVehicleScheduling/DynamicVehicleScheduling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
63 changes: 37 additions & 26 deletions src/DynamicVehicleScheduling/anticipative_solver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,44 @@ 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
Expand Down Expand Up @@ -136,29 +150,25 @@ 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...)

dataset = map(enumerate(T)) do (i, epoch)
routes = epoch_routes[i]
epoch_customers = epoch_indices[epoch]
epoch_customers = epoch_indices[i]

y_true = VSPSolution(
Vector{Int}[
Expand All @@ -167,7 +177,7 @@ function anticipative_solver(
max_index=length(epoch_customers),
).edge_matrix

location_indices = indices[epoch_customers]
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]
Expand All @@ -184,7 +194,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
Expand All @@ -193,6 +203,7 @@ function anticipative_solver(
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,
Expand Down
44 changes: 1 addition & 43 deletions src/DynamicVehicleScheduling/plot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 54 additions & 1 deletion src/DynamicVehicleScheduling/state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -180,6 +181,58 @@ 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
throw(
ErrorException(
"Invalid route: multiple outgoing edges from location $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
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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...,
Expand Down
5 changes: 5 additions & 0 deletions test/dynamic_vsp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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