Skip to content

Commit 149c04e

Browse files
committed
Transform Sto vsp benchmark into an AbstractStochasticBenchmark{true}
1 parent 5cee253 commit 149c04e

6 files changed

Lines changed: 287 additions & 41 deletions

File tree

src/DynamicVehicleScheduling/scenario.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function Utils.generate_scenario(
5151
end
5252

5353
function Utils.generate_scenario(
54-
::DynamicVehicleSchedulingBenchmark, rng::AbstractRNG; instance, kwargs...
54+
::DynamicVehicleSchedulingBenchmark, rng::AbstractRNG; instance::Instance, kwargs...
5555
)
5656
return generate_scenario(instance; rng)
5757
end

src/StochasticVehicleScheduling/StochasticVehicleScheduling.jl

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export plot_instance, plot_solution
66
export compact_linearized_mip,
77
compact_mip, column_generation_algorithm, local_search, deterministic_mip
88
export evaluate_solution, is_feasible
9+
export VSPScenario, build_stochastic_instance
910

1011
using ..Utils
1112
using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES
@@ -30,7 +31,7 @@ using JuMP:
3031
using Plots: Plots, plot, plot!, scatter!, annotate!, text
3132
using Printf: @printf
3233
using Random: Random, AbstractRNG, MersenneTwister
33-
using SparseArrays: sparse
34+
using SparseArrays: sparse, SparseMatrixCSC, nnz
3435
using Statistics: quantile, mean
3536

3637
include("utils.jl")
@@ -41,6 +42,8 @@ include("instance/city.jl")
4142
include("instance/features.jl")
4243
include("instance/instance.jl")
4344

45+
include("scenario.jl")
46+
4447
include("solution/solution.jl")
4548
include("solution/algorithms/mip.jl")
4649
include("solution/algorithms/column_generation.jl")
@@ -57,17 +60,53 @@ Data structure for a stochastic vehicle scheduling benchmark.
5760
# Fields
5861
$TYPEDFIELDS
5962
"""
60-
@kwdef struct StochasticVehicleSchedulingBenchmark <: AbstractBenchmark
63+
@kwdef struct StochasticVehicleSchedulingBenchmark <: AbstractStochasticBenchmark{true}
6164
"number of tasks in each instance"
6265
nb_tasks::Int = 25
63-
"number of scenarios in each instance"
66+
"number of scenarios in each instance (only used to compute features and objective evaluation)"
6467
nb_scenarios::Int = 10
6568
end
6669

70+
include("policies.jl")
71+
6772
function Utils.objective_value(
6873
::StochasticVehicleSchedulingBenchmark, sample::DataSample, y::BitVector
6974
)
70-
return evaluate_solution(y, sample.instance)
75+
stoch = build_stochastic_instance(sample.instance, sample.extra.scenarios)
76+
return evaluate_solution(y, stoch)
77+
end
78+
79+
"""
80+
$TYPEDSIGNATURES
81+
82+
Draw a single fresh [`VSPScenario`](@ref) for the given instance.
83+
Requires `store_city=true` (the default) when generating instances.
84+
"""
85+
function Utils.generate_scenario(
86+
::StochasticVehicleSchedulingBenchmark, rng::AbstractRNG; instance::Instance, kwargs...
87+
)
88+
@assert !isnothing(instance.city) "`generate_scenario` requires `store_city=true`"
89+
return draw_scenario(instance.city, instance.graph, rng)
90+
end
91+
92+
"""
93+
$TYPEDSIGNATURES
94+
"""
95+
function Utils.generate_baseline_policies(bench::StochasticVehicleSchedulingBenchmark)
96+
return svs_generate_baseline_policies(bench)
97+
end
98+
99+
"""
100+
$TYPEDSIGNATURES
101+
102+
Return the anticipative solver: a callable `(scenario::VSPScenario; instance, kwargs...) -> y`
103+
that solves the 1-scenario stochastic VSP via column generation.
104+
"""
105+
function Utils.generate_anticipative_solver(::StochasticVehicleSchedulingBenchmark)
106+
return (scenario::VSPScenario; instance::Instance, kwargs...) -> begin
107+
stochastic_inst = build_stochastic_instance(instance, [scenario])
108+
return column_generation_algorithm(stochastic_inst)
109+
end
71110
end
72111

73112
"""
@@ -84,7 +123,9 @@ policy = sample -> DataSample(; sample.context..., x=sample.x,
84123
dataset = generate_dataset(benchmark, N; target_policy=policy)
85124
```
86125
87-
If `store_city=false`, coordinates and city information are not stored in the instance.
126+
If `store_city=false`, coordinates and city information are not stored in the instance,
127+
and `generate_scenario` will not work. This can be used to save memory if you only need to evaluate
128+
solutions on a fixed set of scenarios.
88129
"""
89130
function Utils.generate_instance(
90131
benchmark::StochasticVehicleSchedulingBenchmark,

src/StochasticVehicleScheduling/instance/instance.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ end
2626
"""
2727
$TYPEDSIGNATURES
2828
29+
Display a compact summary of an [`Instance`](@ref): number of tasks, scenarios, and edges.
30+
"""
31+
function Base.show(io::IO, instance::Instance)
32+
return print(
33+
io,
34+
"VSP Instance($(get_nb_tasks(instance)) tasks, $(get_nb_scenarios(instance)) scenarios, $(ne(instance.graph)) edges)",
35+
)
36+
end
37+
38+
"""
39+
$TYPEDSIGNATURES
40+
2941
Return the acyclic directed graph corresponding to `city`.
3042
Each vertex represents a task. Vertices are ordered by start time of corresponding task.
3143
There is an edge from task u to task v the (end time of u + tie distance between u and v <= start time of v).
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
$TYPEDSIGNATURES
3+
4+
SAA baseline policy: builds a stochastic instance from all K scenarios and solves
5+
via column generation.
6+
Returns a single labeled [`DataSample`](@ref) with `extra=(; scenarios)`.
7+
"""
8+
function svs_saa_policy(sample, scenarios)
9+
stochastic_inst = build_stochastic_instance(sample.instance, scenarios)
10+
y = column_generation_algorithm(stochastic_inst)
11+
return [DataSample(; sample.context..., x=sample.x, y, extra=(; scenarios))]
12+
end
13+
14+
"""
15+
$TYPEDSIGNATURES
16+
17+
Deterministic baseline policy: solves the deterministic MIP (ignores scenario delays).
18+
Returns a single labeled [`DataSample`](@ref) with `extra=(; scenarios)`.
19+
"""
20+
function svs_deterministic_policy(sample, scenarios; model_builder=highs_model)
21+
y = deterministic_mip(sample.instance; model_builder)
22+
return [DataSample(; sample.context..., x=sample.x, y, extra=(; scenarios))]
23+
end
24+
25+
"""
26+
$TYPEDSIGNATURES
27+
28+
Local search baseline policy: builds a stochastic instance from all K scenarios and
29+
solves via local search heuristic.
30+
Returns a single labeled [`DataSample`](@ref) with `extra=(; scenarios)`.
31+
"""
32+
function svs_local_search_policy(sample, scenarios)
33+
stochastic_inst = build_stochastic_instance(sample.instance, scenarios)
34+
y = local_search(stochastic_inst)
35+
return [DataSample(; sample.context..., x=sample.x, y, extra=(; scenarios))]
36+
end
37+
38+
"""
39+
$TYPEDSIGNATURES
40+
41+
Return the named baseline policies for [`StochasticVehicleSchedulingBenchmark`](@ref).
42+
Each policy has signature `(sample, scenarios) -> Vector{DataSample}`.
43+
"""
44+
function svs_generate_baseline_policies(::StochasticVehicleSchedulingBenchmark)
45+
return (;
46+
deterministic=Policy(
47+
"Deterministic MIP", "Ignores delays", svs_deterministic_policy
48+
),
49+
saa=Policy("SAA (col gen)", "Stochastic MIP over K scenarios", svs_saa_policy),
50+
local_search=Policy(
51+
"Local search", "Heuristic with K scenarios", svs_local_search_policy
52+
),
53+
)
54+
end
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""
2+
$TYPEDEF
3+
4+
Represents a single scenario for the stochastic vehicle scheduling problem.
5+
6+
# Fields
7+
$TYPEDFIELDS
8+
"""
9+
struct VSPScenario
10+
"delays per task (length = nb_tasks + 2): scenario_end_time - nominal_end_time"
11+
delays::Vector{Float64}
12+
"scalar slack per edge for this scenario"
13+
slacks::SparseMatrixCSC{Float64,Int}
14+
end
15+
16+
"""
17+
$TYPEDSIGNATURES
18+
19+
Display a compact summary of a [`VSPScenario`](@ref): number of tasks and edges.
20+
"""
21+
function Base.show(io::IO, s::VSPScenario)
22+
return print(io, "VSPScenario($(length(s.delays) - 2) tasks)")
23+
end
24+
25+
"""
26+
$TYPEDSIGNATURES
27+
28+
Draw a single fresh scenario from the city's random distributions,
29+
independently of the stored scenario draws in the `City` struct.
30+
"""
31+
function draw_scenario(city::City, graph::AbstractGraph, rng::AbstractRNG)
32+
tasks = city.tasks
33+
N = length(tasks)
34+
35+
# 1. Draw inter-area factors for 24 hours (single scenario)
36+
inter_area = zeros(24)
37+
previous = 0.0
38+
for h in 1:24
39+
previous = (previous + 0.1) * rand(rng, city.random_inter_area_factor)
40+
inter_area[h] = previous
41+
end
42+
43+
# 2. Draw district delays for each district and 24 hours (single scenario)
44+
nb_per_edge = size(city.districts, 1)
45+
district_delays = [zeros(24) for _ in 1:nb_per_edge, _ in 1:nb_per_edge]
46+
for x in 1:nb_per_edge
47+
for y in 1:nb_per_edge
48+
prev = 0.0
49+
for h in 1:24
50+
prev = scenario_next_delay(prev, city.districts[x, y].random_delay, rng)
51+
district_delays[x, y][h] = prev
52+
end
53+
end
54+
end
55+
56+
# 3. Draw task start times (single scenario per task)
57+
scenario_start_time = [t.start_time + rand(rng, t.random_delay) for t in tasks]
58+
59+
# 4. Compute task end times for job tasks (indices 2:(N-1))
60+
scenario_end_time = [t.end_time for t in tasks]
61+
for i in 2:(N - 1)
62+
task = tasks[i]
63+
origin_x, origin_y = get_district(task.start_point, city)
64+
dest_x, dest_y = get_district(task.end_point, city)
65+
66+
ξ₁ = scenario_start_time[i]
67+
ξ₂ = ξ₁ + district_delays[origin_x, origin_y][hour_of(ξ₁)]
68+
ξ₃ = ξ₂ + (task.end_time - task.start_time) + inter_area[hour_of(ξ₂)]
69+
scenario_end_time[i] = ξ₃ + district_delays[dest_x, dest_y][hour_of(ξ₃)]
70+
end
71+
72+
# 5. Compute delays: scenario_end_time - nominal_end_time
73+
delays = scenario_end_time .- [t.end_time for t in tasks]
74+
75+
# 6. Compute scalar slack for each edge in this scenario
76+
I_idx = [src(e) for e in edges(graph)]
77+
J_idx = [dst(e) for e in edges(graph)]
78+
slack_vals = map(edges(graph)) do e
79+
u = src(e)
80+
v = dst(e)
81+
origin_x, origin_y = get_district(tasks[u].end_point, city)
82+
dest_x, dest_y = get_district(tasks[v].start_point, city)
83+
ξ₁ = scenario_end_time[u]
84+
ξ₂ = ξ₁ + district_delays[origin_x, origin_y][hour_of(ξ₁)]
85+
ξ₃ =
86+
ξ₂ +
87+
distance(tasks[u].end_point, tasks[v].start_point) +
88+
inter_area[hour_of(ξ₂)]
89+
perturbed_arrival = ξ₃ + district_delays[dest_x, dest_y][hour_of(ξ₃)]
90+
perturbed_travel_time = perturbed_arrival - ξ₁
91+
return (v < N ? scenario_start_time[v] : Inf) -
92+
(tasks[u].end_time + perturbed_travel_time)
93+
end
94+
slacks = sparse(I_idx, J_idx, slack_vals, N, N)
95+
96+
return VSPScenario(delays, slacks)
97+
end
98+
99+
"""
100+
$TYPEDSIGNATURES
101+
102+
Build a stochastic [`Instance`](@ref) from a base instance and a vector of fresh
103+
[`VSPScenario`](@ref)s. Each scenario contributes one column to the `intrinsic_delays`
104+
matrix and one entry per edge to the `slacks` sparse matrix.
105+
"""
106+
function build_stochastic_instance(instance::Instance, scenarios::Vector{VSPScenario})
107+
K = length(scenarios)
108+
nb_nodes = length(first(scenarios).delays)
109+
intrinsic_delays = Matrix{Float64}(undef, nb_nodes, K)
110+
for (k, s) in enumerate(scenarios)
111+
intrinsic_delays[:, k] = s.delays
112+
end
113+
114+
graph = instance.graph
115+
N = nv(graph)
116+
I_idx = [src(e) for e in edges(graph)]
117+
J_idx = [dst(e) for e in edges(graph)]
118+
slack_vecs = [[scenarios[k].slacks[src(e), dst(e)] for k in 1:K] for e in edges(graph)]
119+
new_slacks = sparse(I_idx, J_idx, slack_vecs, N, N)
120+
121+
return Instance(
122+
graph,
123+
instance.features,
124+
new_slacks,
125+
intrinsic_delays,
126+
instance.vehicle_cost,
127+
instance.delay_cost,
128+
instance.city,
129+
)
130+
end

0 commit comments

Comments
 (0)