Skip to content

Commit baefa9a

Browse files
committed
Improve DVSP benchmark: additional features, + more variety in start times
1 parent 14bc0f1 commit baefa9a

5 files changed

Lines changed: 200 additions & 12 deletions

File tree

src/DynamicVehicleScheduling/DynamicVehicleScheduling.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ function Utils.generate_statistical_model(
115115
b::DynamicVehicleSchedulingBenchmark; seed=nothing
116116
)
117117
Random.seed!(seed)
118-
return Chain(Dense((b.two_dimensional_features ? 2 : 14) => 1), vec)
118+
return Chain(Dense((b.two_dimensional_features ? 2 : 27) => 1), vec)
119119
end
120120

121121
export DynamicVehicleSchedulingBenchmark

src/DynamicVehicleScheduling/features.jl

Lines changed: 172 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,157 @@
1+
function must_dispatch_in_zone(state::DVSPState)
2+
(; state_instance, is_must_dispatch) = state
3+
4+
startTimes = state_instance.start_time
5+
serviceTimes = state_instance.service_time
6+
durations = state_instance.duration
7+
8+
n = length(startTimes)
9+
must_dispatch_counts = zeros(n)
10+
11+
# For each customer j
12+
for j in 1:n
13+
# Count how many must-dispatch customers i can reach j
14+
for i in 2:n
15+
if is_must_dispatch[i] && i != j
16+
# Check if customer i can reach customer j in time
17+
if startTimes[i] + serviceTimes[i] + durations[i, j] < startTimes[j]
18+
must_dispatch_counts[j] += 1
19+
end
20+
end
21+
end
22+
end
23+
24+
return must_dispatch_counts
25+
end
26+
27+
function count_reachable_from(state::DVSPState)
28+
(; state_instance) = state
29+
30+
startTimes = state_instance.start_time
31+
serviceTimes = state_instance.service_time
32+
durations = state_instance.duration
33+
34+
n = length(startTimes)
35+
reachable_counts = zeros(n)
36+
37+
# For each customer j
38+
for j in 1:n
39+
# Count how many customers i are reachable from j
40+
for i in 2:n
41+
if i != j
42+
# Check if customer i can reach customer j in time
43+
if startTimes[j] + serviceTimes[j] + durations[j, i] < startTimes[i]
44+
reachable_counts[j] += 1
45+
end
46+
end
47+
end
48+
end
49+
50+
return reachable_counts
51+
end
52+
53+
function count_reachable_to(state::DVSPState)
54+
(; state_instance) = state
55+
56+
startTimes = state_instance.start_time
57+
serviceTimes = state_instance.service_time
58+
durations = state_instance.duration
59+
60+
n = length(startTimes)
61+
reachable_counts = zeros(n)
62+
63+
# For each customer j
64+
for j in 1:n
65+
# Count how many customers i can reach j
66+
for i in 2:n
67+
if i != j
68+
# Check if customer i can reach customer j in time
69+
if startTimes[i] + serviceTimes[i] + durations[i, j] < startTimes[j]
70+
reachable_counts[j] += 1
71+
end
72+
end
73+
end
74+
end
75+
76+
return reachable_counts
77+
end
78+
79+
function quantile_reachable_new_requests(
80+
state::DVSPState,
81+
instance::Instance;
82+
n_samples::Int=100,
83+
quantiles=[i * 0.1 for i in 1:9],
84+
)
85+
(; state_instance, current_epoch) = state
86+
(; static_instance, epoch_duration, Δ_dispatch, max_requests_per_epoch) = instance
87+
88+
startTimes = state_instance.start_time
89+
serviceTimes = state_instance.service_time
90+
durations = state_instance.duration
91+
n_current = length(startTimes)
92+
93+
# Time window for next epoch
94+
next_time = epoch_duration * current_epoch + Δ_dispatch
95+
min_time = minimum(static_instance.start_time)
96+
max_time = maximum(static_instance.start_time)
97+
N = customer_count(static_instance)
98+
99+
# Store reachability percentages for each customer across samples
100+
reachability_matrix = zeros(Float64, n_current, n_samples)
101+
102+
rng = MersenneTwister(42)
103+
for s in 1:n_samples
104+
# Sample new requests similar to scenario generation
105+
coordinate_indices = sample_indices(rng, max_requests_per_epoch, N)
106+
sampled_start_times = sample_times(
107+
rng, max_requests_per_epoch, max(min_time, next_time), max_time
108+
)
109+
service_time_indices = sample_indices(rng, max_requests_per_epoch, N)
110+
111+
# Check feasibility (can reach from depot)
112+
depot_durations = static_instance.duration[1, coordinate_indices]
113+
is_feasible = next_time .+ depot_durations .<= sampled_start_times
114+
115+
feasible_coords = coordinate_indices[is_feasible]
116+
feasible_start_times = sampled_start_times[is_feasible]
117+
feasible_service_times = static_instance.service_time[service_time_indices[is_feasible]]
118+
119+
n_new = length(feasible_coords)
120+
if n_new == 0
121+
continue # No reachable requests in this sample
122+
end
123+
124+
# For each current customer, count how many new requests it can reach
125+
for j in 1:n_current
126+
reachable_count = 0
127+
for k in 1:n_new
128+
# Get duration from current customer location to new request location
129+
customer_loc = state.location_indices[j]
130+
new_loc = feasible_coords[k]
131+
travel_time = static_instance.duration[customer_loc, new_loc]
132+
travel_time_back = static_instance.duration[new_loc, customer_loc]
133+
134+
# Check if customer j can reach new request k or if k can reach j
135+
if startTimes[j] + serviceTimes[j] + travel_time <
136+
feasible_start_times[k] ||
137+
startTimes[j] >
138+
feasible_start_times[k] + feasible_service_times[k] + travel_time_back
139+
reachable_count += 1
140+
end
141+
end
142+
reachability_matrix[j, s] = reachable_count / n_new
143+
end
144+
end
145+
146+
# Compute quantiles for each customer
147+
quantile_features = zeros(Float64, n_current, length(quantiles))
148+
for j in 1:n_current
149+
quantile_features[j, :] = quantile(reachability_matrix[j, :], quantiles)
150+
end
151+
152+
return quantile_features
153+
end
154+
1155
function get_features_quantileTimeToRequests(state::DVSPState, instance::Instance)
2156
quantiles = [i * 0.1 for i in 1:9]
3157
a = instance.static_instance.duration[state.location_indices, 2:end]
@@ -6,7 +160,7 @@ function get_features_quantileTimeToRequests(state::DVSPState, instance::Instanc
6160
end
7161

8162
function compute_model_free_features(state::DVSPState, instance::Instance)
9-
(; state_instance, is_postponable) = state
163+
(; state_instance, is_postponable, is_must_dispatch) = state
10164

11165
startTimes = state_instance.start_time
12166
endTimes = startTimes .+ state_instance.service_time
@@ -15,19 +169,34 @@ function compute_model_free_features(state::DVSPState, instance::Instance)
15169

16170
slack_next_epoch = startTimes .- instance.epoch_duration
17171

172+
must_dispatch_counts = must_dispatch_in_zone(state)
173+
nb_must_dispatch = sum(is_must_dispatch)
174+
if nb_must_dispatch > 0
175+
must_dispatch_counts ./= nb_must_dispatch
176+
end
177+
178+
reachable_to_ratios = count_reachable_to(state) ./ (length(startTimes) - 1)
179+
reachable_from_ratios = count_reachable_from(state) ./ (length(startTimes) - 1)
180+
reachable_ratios = reachable_to_ratios .+ reachable_from_ratios
181+
18182
model_free_features = hcat(
19183
startTimes[is_postponable], # 1
20184
endTimes[is_postponable], # 2
21185
timeDepotRequest[is_postponable], # 3
22186
timeRequestDepot[is_postponable], # 4
23-
slack_next_epoch[is_postponable], # 5-14
187+
slack_next_epoch[is_postponable], # 5
188+
must_dispatch_counts[is_postponable], # 6
189+
reachable_to_ratios[is_postponable], # 7
190+
reachable_from_ratios[is_postponable], # 8
191+
reachable_ratios[is_postponable], # 9
24192
)
25193
return model_free_features
26194
end
27195

28196
function compute_model_aware_features(state::DVSPState, instance::Instance)
29197
quantileTimeToRequests = get_features_quantileTimeToRequests(state, instance)
30-
model_aware_features = quantileTimeToRequests
198+
quantileReachableNewRequests = quantile_reachable_new_requests(state, instance)
199+
model_aware_features = hcat(quantileTimeToRequests, quantileReachableNewRequests)
31200
return model_aware_features[state.is_postponable, :]
32201
end
33202

src/DynamicVehicleScheduling/scenario.jl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,24 @@ function Utils.generate_scenario(
2929
new_service_time = Vector{Float64}[]
3030
new_start_time = Vector{Float64}[]
3131

32+
min_time, max_time = extrema(start_time)
3233
for epoch in 1:last_epoch
3334
time = epoch_duration * (epoch - 1) + Δ_dispatch
3435

3536
coordinate_indices = sample_indices(rng, max_requests_per_epoch, N)
36-
start_time_indices = sample_indices(rng, max_requests_per_epoch, N)
37+
# sampled_start_time = sample_times(rng, max_requests_per_epoch, min_time, max_time)
38+
sampled_start_time = sample_times(
39+
rng, max_requests_per_epoch, max(min_time, time), max_time
40+
)
41+
# start_time_indices = sample_indices(rng, max_requests_per_epoch, N)
3742
service_time_indices = sample_indices(rng, max_requests_per_epoch, N)
3843

39-
is_feasible =
40-
time .+ duration[depot, coordinate_indices] .<= start_time[start_time_indices]
44+
is_feasible = time .+ duration[depot, coordinate_indices] .<= sampled_start_time # start_time[start_time_indices]
4145

4246
push!(new_indices, coordinate_indices[is_feasible])
4347
push!(new_service_time, service_time[service_time_indices[is_feasible]])
44-
push!(new_start_time, start_time[start_time_indices[is_feasible]])
48+
push!(new_start_time, sampled_start_time[is_feasible])
49+
# push!(new_start_time, start_time[start_time_indices[is_feasible]])
4550
end
4651
return Scenario(new_indices, new_service_time, new_start_time)
4752
end

src/DynamicVehicleScheduling/static_vsp/parsing.jl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ It uses time window values to compute task times as the middle of the interval.
77
Round all values to `Int` if `rounded=true`.
88
Normalize all time values by the `normalization` parameter.
99
"""
10-
function read_vsp_instance(filepath::String; rounded::Bool=false, normalization=3600.0)
11-
type = rounded ? Int : Float64
10+
function read_vsp_instance(filepath::String; normalization=3600.0, digits=2)
11+
type = Float64 #rounded ? Int : Float64
1212
mode = ""
1313
local edge_weight_type
1414
local edge_weight_format
@@ -84,12 +84,17 @@ function read_vsp_instance(filepath::String; rounded::Bool=false, normalization=
8484
duration = mapreduce(permutedims, vcat, duration_matrix)
8585

8686
coordinate = [
87-
Point(x / normalization, y / normalization) for
87+
Point(round(x / normalization; digits), round(y / normalization; digits)) for
8888
(x, y) in zip(coordinates[:, 1], coordinates[:, 2])
8989
]
9090
service_time ./= normalization
9191
start_time ./= normalization
9292
duration ./= normalization
9393

94-
return StaticInstance(; coordinate, service_time, start_time, duration)
94+
return StaticInstance(;
95+
coordinate,
96+
service_time=round.(service_time; digits),
97+
start_time=round.(start_time; digits),
98+
duration=round.(duration; digits),
99+
)
95100
end

src/DynamicVehicleScheduling/utils.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ sample_indices(rng::AbstractRNG, k, N) = randperm(rng, N)[1:k] .+ 1
88
"""
99
$TYPEDSIGNATURES
1010
11+
Sample k random time values between min_time and max_time.
12+
"""
13+
function sample_times(rng::AbstractRNG, k, min_time, max_time; digits=2)
14+
return round.(min_time .+ (max_time - min_time) .* rand(rng, k); digits=digits)
15+
end
16+
17+
"""
18+
$TYPEDSIGNATURES
19+
1120
Compute the total cost of a set of routes given a distance matrix, i.e. the sum of the distances between each location in the route.
1221
Note that the first location is implicitly assumed to be the depot, and should not appear in the route.
1322
"""

0 commit comments

Comments
 (0)