Skip to content

Commit c159731

Browse files
committed
update
1 parent 6fd1ff4 commit c159731

24 files changed

+1558
-2
lines changed

src/DecisionFocusedLearningBenchmarks.jl

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,42 @@ module DecisionFocusedLearningBenchmarks
33
using DataDeps
44
using Requires: @require
55

6+
function _euro_neurips_unpack(local_filepath)
7+
directory = dirname(local_filepath)
8+
unpack(local_filepath)
9+
# Move instances and delete the rest
10+
for filepath in readdir(
11+
joinpath(directory, "euro-neurips-vrp-2022-quickstart-main", "instances"); join=true
12+
)
13+
if endswith(filepath, ".txt")
14+
mv(filepath, joinpath(directory, basename(filepath)))
15+
end
16+
end
17+
rm(joinpath(directory, "euro-neurips-vrp-2022-quickstart-main"); recursive=true)
18+
return nothing
19+
end
20+
621
function __init__()
722
# Register the Warcraft dataset
823
ENV["DATADEPS_ALWAYS_ACCEPT"] = "true"
924
register(
1025
DataDep(
1126
"warcraft",
12-
"This is the warcraft dataset",
27+
"Warcraft shortest path dataset",
1328
"http://cermics.enpc.fr/~bouvierl/warcraft_TP/data.zip";
1429
post_fetch_method=unpack,
1530
),
1631
)
1732

33+
register(
34+
DataDep(
35+
"euro-neurips-2022",
36+
"EURO-NeurIPs challenge 2022 dataset",
37+
"https://github.com/ortec/euro-neurips-vrp-2022-quickstart/archive/refs/heads/main.zip";
38+
post_fetch_method=_euro_neurips_unpack,
39+
),
40+
)
41+
1842
# Gurobi setup
1943
@info "If you have Gurobi installed and want to use it, make sure to `using Gurobi` in order to enable it."
2044
@require Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" include("gurobi_setup.jl")
@@ -30,6 +54,7 @@ include("Warcraft/Warcraft.jl")
3054
include("FixedSizeShortestPath/FixedSizeShortestPath.jl")
3155
include("PortfolioOptimization/PortfolioOptimization.jl")
3256
include("StochasticVehicleScheduling/StochasticVehicleScheduling.jl")
57+
include("DynamicVehicleScheduling/DynamicVehicleScheduling.jl")
3358

3459
using .Utils
3560
using .Argmax
@@ -39,6 +64,7 @@ using .Warcraft
3964
using .FixedSizeShortestPath
4065
using .PortfolioOptimization
4166
using .StochasticVehicleScheduling
67+
using .DynamicVehicleScheduling
4268

4369
# Interface
4470
export AbstractBenchmark, DataSample
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
$TYPEDSIGNATURES
3+
4+
Retrieve anticipative routes solution from the given MIP solution `y`.
5+
Outputs a set of routes per epoch.
6+
"""
7+
function retrieve_routes_anticipative(y::AbstractArray, dvspenv::DVSPEnv)
8+
nb_tasks = length(dvspenv.customer_index)
9+
(; first_epoch, last_epoch) = dvspenv.config
10+
job_indices = 2:(nb_tasks)
11+
epoch_indices = first_epoch:last_epoch
12+
13+
routes = [Vector{Int}[] for t in epoch_indices]
14+
for t in epoch_indices
15+
start = [i for i in job_indices if y[1, i, t] 1]
16+
for task in start
17+
route = Int[]
18+
current_task = task
19+
while current_task != 1 # < nb_tasks
20+
push!(route, current_task)
21+
local next_task
22+
for i in 1:nb_tasks
23+
if isapprox(y[current_task, i, t], 1; atol=0.1)
24+
next_task = i
25+
break
26+
end
27+
end
28+
current_task = next_task
29+
end
30+
push!(routes[t], route)
31+
end
32+
end
33+
return routes
34+
end
35+
36+
"""
37+
$TYPEDSIGNATURES
38+
39+
Solve the anticipative VSP problem for environment `env`.
40+
For this, it uses the current environment history, so make sure that the environment is terminated before calling this method.
41+
"""
42+
function anticipative_solver(env::DVSPEnv; model_builder=highs_model, draw_epochs=true)
43+
draw_epochs && draw_all_epochs!(env)
44+
(; customer_index, service_time, start_time, request_epoch) = env
45+
duration = env.config.static_instance.duration[customer_index, customer_index]
46+
(; first_epoch, last_epoch, epoch_duration, Δ_dispatch) = env.config
47+
48+
@assert first_epoch == 1
49+
50+
model = model_builder()
51+
set_silent(model)
52+
53+
nb_nodes = length(customer_index)
54+
job_indices = 2:nb_nodes
55+
epoch_indices = first_epoch:last_epoch
56+
57+
@variable(model, y[i=1:nb_nodes, j=1:nb_nodes, t=epoch_indices]; binary=true)
58+
59+
@objective(
60+
model,
61+
Max,
62+
sum(
63+
-duration[i, j] * y[i, j, t] for i in 1:nb_nodes, j in 1:nb_nodes,
64+
t in epoch_indices
65+
)
66+
)
67+
68+
# flow constraint per epoch
69+
for t in epoch_indices, i in 1:nb_nodes
70+
@constraint(
71+
model,
72+
sum(y[j, i, t] for j in 1:nb_nodes) == sum(y[i, j, t] for j in 1:nb_nodes)
73+
)
74+
end
75+
76+
# each task must be done once along the horizon
77+
@constraint(
78+
model,
79+
demand[i in job_indices],
80+
sum(y[j, i, t] for j in 1:nb_nodes, t in epoch_indices) == 1
81+
)
82+
83+
# a trip from i can be planned only after request appeared
84+
for i in job_indices, t in epoch_indices, j in 1:nb_nodes
85+
if t < request_epoch[i]
86+
@constraint(model, y[i, j, t] <= 0)
87+
end
88+
end
89+
90+
# a trip from i can be done only before limit date
91+
for i in job_indices, t in epoch_indices, j in 1:nb_nodes
92+
if (t - 1) * epoch_duration + duration[1, i] + Δ_dispatch > start_time[i] # ! this only works if first_epoch = 1
93+
@constraint(model, y[i, j, t] <= 0)
94+
end
95+
end
96+
97+
# trips can be planned if start, service and transport times enable it
98+
for i in job_indices, t in epoch_indices, j in job_indices
99+
if start_time[i] <= start_time[j]
100+
if start_time[i] + service_time[i] + duration[i, j] > start_time[j]
101+
@constraint(model, y[i, j, t] <= 0)
102+
end
103+
else
104+
@constraint(model, y[i, j, t] <= 0)
105+
end
106+
end
107+
108+
optimize!(model)
109+
110+
return retrieve_routes_anticipative(value.(y), env)
111+
end
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""
2+
$TYPEDSIGNATURES
3+
4+
Create the acyclic digraph associated with the given VSP `instance`.
5+
"""
6+
function create_graph(instance::VSPInstance)
7+
(; duration, start_time, service_time) = instance
8+
# Initialize directed graph
9+
nb_vertices = nb_locations(instance)
10+
graph = SimpleDiGraph(nb_vertices)
11+
12+
depot = 1 # depot is always index 1
13+
customers = 2:nb_vertices # other vertices are customers
14+
15+
# Create existing edges
16+
for i₁ in customers
17+
# link every task to depot
18+
add_edge!(graph, depot, i₁)
19+
add_edge!(graph, i₁, depot)
20+
21+
t₁ = start_time[i₁]
22+
for i₂ in (i₁ + 1):nb_vertices
23+
t₂ = start_time[i₂]
24+
25+
if t₁ <= t₂
26+
if t₁ + service_time[i₁] + duration[i₁, i₂] <= t₂
27+
add_edge!(graph, i₁, i₂)
28+
end
29+
else
30+
if t₂ + service_time[i₂] + duration[i₂, i₁] <= t₁
31+
add_edge!(graph, i₂, i₁)
32+
end
33+
end
34+
end
35+
end
36+
37+
return graph
38+
end
39+
40+
"""
41+
$TYPEDSIGNATURES
42+
43+
Create the acyclic digraph associated with the given VSP `state`.
44+
"""
45+
function create_graph(state::VSPState)
46+
return create_graph(state.instance)
47+
end
48+
49+
"""
50+
$TYPEDSIGNATURES
51+
52+
Retrieve routes solution from the given MIP solution `y` matrix and `graph`.
53+
"""
54+
function retrieve_routes(y::AbstractArray, graph::AbstractGraph)
55+
nb_tasks = nv(graph)
56+
job_indices = 2:(nb_tasks)
57+
routes = Vector{Int}[]
58+
59+
start = [i for i in job_indices if y[1, i] 1]
60+
for task in start
61+
route = Int[]
62+
current_task = task
63+
while current_task != 1 # < nb_tasks
64+
push!(route, current_task)
65+
local next_task
66+
for i in outneighbors(graph, current_task)
67+
if isapprox(y[current_task, i], 1; atol=0.1)
68+
next_task = i
69+
break
70+
end
71+
end
72+
current_task = next_task
73+
end
74+
push!(routes, route)
75+
end
76+
return routes
77+
end
78+
79+
"""
80+
$TYPEDSIGNATURES
81+
82+
Solve the Prize Collecting Vehicle Scheduling Problem defined by `instance` and prize vector `θ`.
83+
"""
84+
function prize_collecting_vsp(
85+
θ::AbstractVector; instance::VSPState, model_builder=highs_model, kwargs...
86+
)
87+
(; duration) = instance.instance
88+
graph = create_graph(instance)
89+
90+
model = model_builder()
91+
set_silent(model)
92+
93+
nb_nodes = nv(graph)
94+
job_indices = 2:(nb_nodes)
95+
96+
@variable(model, y[i=1:nb_nodes, j=1:nb_nodes; has_edge(graph, i, j)] >= 0)
97+
98+
θ_ext = fill(0.0, nb_locations(instance)) # no prize for must dispatch requests, only hard constraints
99+
θ_ext[instance.is_postponable] .= θ
100+
101+
@objective(
102+
model,
103+
Max,
104+
sum(
105+
(θ_ext[dst(edge)] - duration[src(edge), dst(edge)]) * y[src(edge), dst(edge)]
106+
for edge in edges(graph)
107+
)
108+
)
109+
@constraint(
110+
model,
111+
flow[i in 2:nb_nodes],
112+
sum(y[j, i] for j in inneighbors(graph, i)) ==
113+
sum(y[i, j] for j in outneighbors(graph, i))
114+
)
115+
@constraint(
116+
model, demand[i in job_indices], sum(y[j, i] for j in inneighbors(graph, i)) <= 1
117+
)
118+
# must dispatch constraints
119+
@constraint(
120+
model,
121+
demand_must_dispatch[i in job_indices; instance.is_must_dispatch[i]],
122+
sum(y[j, i] for j in inneighbors(graph, i)) == 1
123+
)
124+
125+
optimize!(model)
126+
127+
return retrieve_routes(value.(y), graph)
128+
end
129+
130+
# ?
131+
function prize_collecting_vsp_Q(
132+
θ::AbstractVector,
133+
vals::AbstractVector;
134+
instance::VSPState,
135+
model_builder=highs_model,
136+
kwargs...,
137+
)
138+
(; duration) = instance.instance
139+
graph = create_graph(instance)
140+
model = model_builder()
141+
set_silent(model)
142+
nb_nodes = nv(graph)
143+
job_indices = 2:(nb_nodes)
144+
@variable(model, y[i=1:nb_nodes, j=1:nb_nodes; has_edge(graph, i, j)] >= 0)
145+
θ_ext = fill(0.0, nb_locations(instance.instance)) # no prize for must dispatch requests, only hard constraints
146+
θ_ext[instance.is_postponable] .= θ
147+
# v_ext = fill(0.0, nb_locations(instance.instance)) # no prize for must dispatch requests, only hard constraints
148+
# v_ext[instance.is_postponable] .= vals
149+
@objective(
150+
model,
151+
Max,
152+
sum(
153+
(θ_ext[dst(edge)] + vals[dst(edge)] - duration[src(edge), dst(edge)]) *
154+
y[src(edge), dst(edge)] for edge in edges(graph)
155+
)
156+
)
157+
@constraint(
158+
model,
159+
flow[i in 2:nb_nodes],
160+
sum(y[j, i] for j in inneighbors(graph, i)) ==
161+
sum(y[i, j] for j in outneighbors(graph, i))
162+
)
163+
@constraint(
164+
model, demand[i in job_indices], sum(y[j, i] for j in inneighbors(graph, i)) <= 1
165+
)
166+
# must dispatch constraints
167+
@constraint(
168+
model,
169+
demand_must_dispatch[i in job_indices; instance.is_must_dispatch[i]],
170+
sum(y[j, i] for j in inneighbors(graph, i)) == 1
171+
)
172+
optimize!(model)
173+
return retrieve_routes(value.(y), graph)
174+
end
175+
176+
function my_objective_value(θ, routes; instance)
177+
(; duration) = instance.instance
178+
total = 0.0
179+
θ_ext = fill(0.0, nb_locations(instance))
180+
θ_ext[instance.is_postponable] .= θ
181+
for route in routes
182+
for (u, v) in partition(vcat(1, route), 2, 1)
183+
total += θ_ext[v] - duration[u, v]
184+
end
185+
end
186+
return -total
187+
end
188+
189+
function _objective_value(θ, routes; instance)
190+
(; duration) = instance.instance
191+
total = 0.0
192+
θ_ext = fill(0.0, nb_locations(instance))
193+
θ_ext[instance.is_postponable] .= θ
194+
mapping = cumsum(instance.is_postponable)
195+
g = falses(length(θ))
196+
for route in routes
197+
for (u, v) in partition(vcat(1, route), 2, 1)
198+
total -= duration[u, v]
199+
if instance.is_postponable[v]
200+
total += θ_ext[v]
201+
g[mapping[v]] = 1
202+
end
203+
end
204+
end
205+
return -total, g
206+
end
207+
208+
function ChainRulesCore.rrule(::typeof(my_objective_value), θ, routes; instance)
209+
total, g = _objective_value(θ, routes; instance)
210+
function pullback(dy)
211+
g = g .* dy
212+
return NoTangent(), g, NoTangent()
213+
end
214+
return total, pullback
215+
end

0 commit comments

Comments
 (0)