Skip to content

Commit 507ff4f

Browse files
committed
fix bugs and add column generation
1 parent a232588 commit 507ff4f

File tree

5 files changed

+178
-28
lines changed

5 files changed

+178
-28
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ authors = ["Members of JuliaDecisionFocusedLearning"]
44
version = "0.1.0"
55

66
[deps]
7+
ConstrainedShortestPaths = "b3798467-87dc-4d99-943d-35a1bd39e395"
78
DataDeps = "124859b0-ceae-595e-8997-d05f6a7a8dfe"
89
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
910
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
@@ -27,6 +28,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
2728
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
2829

2930
[compat]
31+
ConstrainedShortestPaths = "0.5.1"
3032
DataDeps = "0.7"
3133
Distributions = "0.25"
3234
DocStringExtensions = "0.9"

src/StochasticVehicleScheduling/StochasticVehicleScheduling.jl

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module StochasticVehicleScheduling
22

33
using ..Utils
44
using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES
5+
using ConstrainedShortestPaths:
6+
stochastic_routing_shortest_path, stochastic_routing_shortest_path_with_threshold
57
using Distributions: Distribution, LogNormal, Uniform
68
using Graphs:
79
AbstractGraph,
@@ -16,7 +18,15 @@ using Graphs:
1618
inneighbors,
1719
outneighbors
1820
using JuMP:
19-
Model, @variable, @objective, @constraint, optimize!, value, objective_value, set_silent
21+
Model,
22+
@variable,
23+
@objective,
24+
@constraint,
25+
optimize!,
26+
value,
27+
objective_value,
28+
set_silent,
29+
dual
2030
using Printf: @printf
2131
using Random: Random, AbstractRNG, MersenneTwister
2232
using SparseArrays: sparse
@@ -32,6 +42,7 @@ include("instance/instance.jl")
3242

3343
include("solution/solution.jl")
3444
include("solution/exact_algorithms/mip.jl")
45+
include("solution/exact_algorithms/column_generation.jl")
3546

3647
"""
3748
$TYPEDFIELDS
@@ -74,6 +85,7 @@ function Utils.generate_statistical_model(bench::StochasticVehicleSchedulingBenc
7485

7586
export StochasticVehicleSchedulingBenchmark
7687

77-
export compact_linearized_mip, compact_mip
88+
export compact_linearized_mip,
89+
compact_mip, column_generation_algorithm, evaluate_solution, is_feasible
7890

7991
end

src/StochasticVehicleScheduling/instance/instance.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ $TYPEDSIGNATURES
8787
Returns the number of scenarios in instance.
8888
"""
8989
function get_nb_scenarios(instance::Instance)
90-
return size(instance.delays, 2)
90+
return size(instance.intrinsic_delays, 2)
9191
end
9292

9393
"""
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""
2+
delay_sum(path, slacks, delays)
3+
4+
Evaluate the total delay along path.
5+
"""
6+
function delay_sum(path, slacks, delays)
7+
nb_scenarios = size(delays, 2)
8+
old_v = path[1]
9+
R = delays[old_v, :]
10+
C = 0.0
11+
for v in path[2:(end - 1)]
12+
@. R = max(R - slacks[old_v, v], 0) + delays[v, :]
13+
C += sum(R) / nb_scenarios
14+
old_v = v
15+
end
16+
return C
17+
end
18+
19+
"""
20+
column_generation(instance::Instance)
21+
22+
Note: If you have Gurobi, use `grb_model` as `model_builder` instead of `glpk_model`.
23+
"""
24+
function column_generation(
25+
instance::Instance; only_relaxation=false, model_builder=highs_model
26+
)
27+
(; graph, slacks, intrinsic_delays, vehicle_cost, delay_cost) = instance
28+
29+
nb_nodes = nv(graph)
30+
job_indices = 2:(nb_nodes - 1)
31+
32+
model = model_builder()
33+
set_silent(model)
34+
35+
@variable(model, λ[v in 1:nb_nodes])
36+
37+
@objective(model, Max, sum(λ[v] for v in job_indices))
38+
39+
initial_paths = [[1, v, nb_nodes] for v in job_indices]
40+
@constraint(
41+
model,
42+
con[p in initial_paths],
43+
delay_cost * delay_sum(p, slacks, intrinsic_delays) + vehicle_cost -
44+
sum(λ[v] for v in job_indices if v in p) >= 0
45+
)
46+
@constraint(model, λ[1] == 0)
47+
@constraint(model, λ[nb_nodes] == 0)
48+
49+
new_paths = Vector{Int}[]
50+
cons = []
51+
52+
while true
53+
optimize!(model)
54+
λ_val = value.(λ)
55+
(; c_star, p_star) = stochastic_routing_shortest_path(
56+
graph, slacks, intrinsic_delays, λ_val ./ delay_cost
57+
)
58+
λ_sum = sum(λ_val[v] for v in job_indices if v in p_star)
59+
path_cost = delay_cost * c_star + λ_sum + vehicle_cost
60+
if path_cost - λ_sum > -1e-10
61+
break
62+
end
63+
push!(new_paths, p_star)
64+
push!(
65+
cons,
66+
@constraint(
67+
model, path_cost - sum(λ[v] for v in job_indices if v in p_star) >= 0
68+
)
69+
)
70+
end
71+
72+
c_low = objective_value(model)
73+
paths = cat(initial_paths, new_paths; dims=1)
74+
c_upp, y, _ = compute_solution_from_selected_columns(instance, paths)
75+
76+
# If relaxation requested or solution is optimal, return
77+
if c_upp c_low || only_relaxation
78+
return value.(λ),
79+
objective_value(model), cat(initial_paths, new_paths; dims=1), dual.(con),
80+
dual.(cons)
81+
end
82+
83+
# else, try to close the gap
84+
threshold = (c_upp - c_low - vehicle_cost) / delay_cost
85+
λ_val = value.(λ)
86+
additional_paths, costs = stochastic_routing_shortest_path_with_threshold(
87+
graph, slacks, intrinsic_delays, λ_val ./ delay_cost; threshold
88+
)
89+
90+
return value.(λ),
91+
objective_value(model),
92+
unique(cat(initial_paths, new_paths, additional_paths; dims=1)),
93+
dual.(con),
94+
dual.(cons)
95+
end
96+
97+
"""
98+
compute_solution_from_selected_columns(instance::Instance, paths[; bin=true])
99+
100+
Note: If you have Gurobi, use `grb_model` as `model_builder` instead od `glpk_model`.
101+
"""
102+
function compute_solution_from_selected_columns(
103+
instance::Instance, paths; bin=true, model_builder=highs_model
104+
)
105+
(; graph, slacks, intrinsic_delays, vehicle_cost, delay_cost) = instance
106+
107+
nb_nodes = nv(graph)
108+
job_indices = 2:(nb_nodes - 1)
109+
110+
model = model_builder()
111+
set_silent(model)
112+
113+
if bin
114+
@variable(model, y[p in paths], Bin)
115+
else
116+
@variable(model, y[p in paths] >= 0)
117+
end
118+
119+
@objective(
120+
model,
121+
Min,
122+
sum(
123+
(delay_cost * delay_sum(p, slacks, intrinsic_delays) + vehicle_cost) * y[p] for
124+
p in paths
125+
)
126+
)
127+
128+
@constraint(model, con[v in job_indices], sum(y[p] for p in paths if v in p) == 1)
129+
130+
optimize!(model)
131+
132+
sol = value.(y)
133+
return objective_value(model), sol, paths[[sol[p] for p in paths] .== 1.0]
134+
end
135+
136+
function column_generation_algorithm(instance::Instance)
137+
_, _, columns, _, _ = column_generation(instance)
138+
_, _, sol = compute_solution_from_selected_columns(instance, columns)
139+
col_solution = solution_from_paths(sol, instance)
140+
return col_solution
141+
end

src/StochasticVehicleScheduling/solution/solution.jl

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# TODO: only keep value field ?
22
"""
3-
Solution
3+
$TYPEDEF
44
55
Should always be associated with an `Instance`.
66
77
# Fields
8-
- `value::BitVector`: for each graph edge of instance, 1 if selected, else 0
9-
- `path_value::BitMatrix`: each row represents a vehicle, each column a task.
10-
1 if task is done by the vehicle, else 0
8+
$TYPEDFIELDS
119
"""
1210
struct Solution
11+
"for each graph edge of instance, 1 if selected, else 0"
1312
value::BitVector # for every edge of graph 1 if selected, else 0
13+
"each row represents a vehicle, each column a task.
14+
1 if task is done by the vehicle, else 0"
1415
path_value::BitMatrix # list of vehicles paths
1516
end
1617

@@ -23,7 +24,7 @@ function get_nb_vehicles(solution::Solution)
2324
end
2425

2526
"""
26-
get_routes(solution::Solution)
27+
$TYPEDSIGNATURES
2728
2829
Compute routes of solution.
2930
"""
@@ -42,7 +43,7 @@ function get_routes(solution::Solution)
4243
end
4344

4445
"""
45-
Solution(value::BitVector, instance::Instance)
46+
$TYPEDSIGNATURES
4647
4748
Create a Solution from a BitVector value.
4849
"""
@@ -61,7 +62,7 @@ function Solution(value::BitVector, instance::Instance)
6162
end
6263

6364
"""
64-
Solution(value::BitVector, instance::Instance)
65+
$TYPEDSIGNATURES
6566
6667
Create a Solution from a BitMatrix path value.
6768
"""
@@ -81,7 +82,7 @@ function Solution(path_value::BitMatrix, instance::Instance)
8182
end
8283

8384
"""
84-
solution_from_paths(paths, instance::Instance)
85+
$TYPEDSIGNATURES
8586
8687
Create a Solution from routes.
8788
"""
@@ -103,7 +104,7 @@ function solution_from_paths(paths, instance::Instance)
103104
end
104105

105106
"""
106-
solution_from_paths(paths, instance::Instance)
107+
$TYPEDSIGNATURES
107108
108109
Create a Solution from a JuMP array.
109110
"""
@@ -161,7 +162,7 @@ function basic_path_solution(graph::AbstractGraph)
161162
end
162163

163164
"""
164-
basic_solution(graph::AbstractGraph)
165+
$TYPEDSIGNATURES
165166
166167
Create a solution with one vehicle per task.
167168
"""
@@ -179,13 +180,7 @@ function basic_solution(instance::Instance)
179180
end
180181

181182
"""
182-
evaluate_task(
183-
i_task::Integer,
184-
instance::Instance,
185-
old_task_index::Integer,
186-
old_delay::Real,
187-
scenario::Int,
188-
)
183+
$TYPEDSIGNATURES
189184
190185
Evaluate the total delay of task `i_task` in `scenario`, knowing that current delay from task
191186
`old_task_index` is `old_delay`.
@@ -197,9 +192,9 @@ function evaluate_task(
197192
old_delay::Real,
198193
scenario::Int,
199194
)
200-
(; slacks, delays) = instance
195+
(; slacks, intrinsic_delays) = instance
201196

202-
delay = delays[i_task, scenario]
197+
delay = intrinsic_delays[i_task, scenario]
203198
slack = slacks[old_task_index, i_task][scenario]
204199

205200
return delay + max(old_delay - slack, 0)
@@ -240,7 +235,7 @@ function evaluate_scenario(path_value::BitMatrix, instance::Instance, scenario_i
240235
end
241236

242237
"""
243-
evaluate_scenario(solution::Solution, instance::Instance, scenario_index::Int)
238+
$TYPEDSIGNATURES
244239
245240
Compute total delay of scenario.
246241
"""
@@ -249,7 +244,7 @@ function evaluate_scenario(solution::Solution, instance::Instance, scenario_inde
249244
end
250245

251246
"""
252-
evaluate_scenario(path_value::BitMatrix, instance::Instance, scenario_index::Int)
247+
$TYPEDSIGNATURES
253248
254249
Compute total weighted objective of solution.
255250
"""
@@ -267,7 +262,7 @@ function evaluate_solution(path_value::BitMatrix, instance::Instance)
267262
end
268263

269264
"""
270-
evaluate_scenario(path_value::BitMatrix, instance::Instance, scenario_index::Int)
265+
$TYPEDSIGNATURES
271266
272267
Compute total weighted objective of solution.
273268
"""
@@ -314,7 +309,7 @@ function to_array(path_value::BitMatrix, instance::Instance)
314309
end
315310

316311
"""
317-
to_array(solution::Solution, instance::Instance)
312+
$TYPEDSIGNATURES
318313
319314
Returns a BitMatrix, with value true at each index (i, j) if corresponding edge of graph
320315
is selected in the solution
@@ -324,11 +319,11 @@ function to_array(solution::Solution, instance::Instance)
324319
end
325320

326321
"""
327-
is_admissible(solution::Solution, instance::Instance)
322+
$TYPEDSIGNATURES
328323
329324
Check if `solution` is an admissible solution of `instance`.
330325
"""
331-
function is_admissible(solution::Solution, instance::Instance)
326+
function is_feasible(solution::Solution, instance::Instance)
332327
graph = instance.graph
333328
nb_nodes = nv(graph)
334329
nb_tasks = nb_nodes - 2

0 commit comments

Comments
 (0)