///|
/// Weights used when ranking related customers for destroy operators.
pub struct RelatednessWeights {
distance : Double
demand : Double
time_window : Double
service : Double
} derive(Debug, ToJson)
///|
pub fn RelatednessWeights::default() -> RelatednessWeights {
{ distance: 1.0, demand: 1.5, time_window: 0.1, service: 0.2 }
}
///|
pub fn RelatednessWeights::tune(
self : RelatednessWeights,
distance? : Double = self.distance,
demand? : Double = self.demand,
time_window? : Double = self.time_window,
service? : Double = self.service,
) -> RelatednessWeights {
{ distance, demand, time_window, service }
}
///|
pub fn RelatednessWeights::distance(self : RelatednessWeights) -> Double {
self.distance
}
///|
pub fn RelatednessWeights::demand(self : RelatednessWeights) -> Double {
self.demand
}
///|
pub fn RelatednessWeights::time_window(self : RelatednessWeights) -> Double {
self.time_window
}
///|
pub fn RelatednessWeights::service(self : RelatednessWeights) -> Double {
self.service
}
///|
/// Scores two customers by spatial, demand, and temporal similarity.
pub fn relatedness_score(
instance : @core.VrpInstance,
left : Int,
right : Int,
weights : RelatednessWeights,
) -> Double {
let left_customer = instance.customer(left)
let right_customer = instance.customer(right)
weights.distance() * instance.distance(left, right) +
weights.demand() *
Double::abs(left_customer.demand() - right_customer.demand()) +
weights.time_window() *
Double::abs(left_customer.ready_time() - right_customer.ready_time()) +
weights.service() *
Double::abs(left_customer.service_time() - right_customer.service_time())
}
///|
/// Returns customer indices ordered from most to least related to a seed.
pub fn rank_related_customers(
instance : @core.VrpInstance,
seed : Int,
candidates : Array[Int],
weights : RelatednessWeights,
) -> Array[Int] {
let ranked = candidates.copy()
ranked.sort_by(fn(left, right) {
let order = Double::compare(
relatedness_score(instance, seed, left, weights),
relatedness_score(instance, seed, right, weights),
)
if order == 0 {
left - right
} else {
order
}
})
ranked
}
///|
/// Measures how evenly a solution uses its vehicle capacities.
pub fn route_balance_score(
instance : @core.VrpInstance,
solution : @core.Solution,
) -> Double {
let loads = solution.route_loads()
if loads.is_empty() || instance.vehicle().capacity <= 0.0 {
0.0
} else {
let average = loads.fold(init=0.0, fn(total, load) { total + load }) /
Double::from_int(loads.length())
let deviation = loads.fold(init=0.0, fn(total, load) {
total + Double::abs(load - average)
})
1.0 -
deviation / (Double::from_int(loads.length()) * instance.vehicle().capacity)
}
}
///|
/// Scores a candidate with a quality/constraint trade-off for dashboards.
pub fn operator_candidate_score(
instance : @core.VrpInstance,
solution : @core.Solution,
balance_weight : Double,
) -> Double {
solution.objective_value() -
balance_weight * route_balance_score(instance, solution)
}
///|
/// Selects a destroy operator from a score vector with deterministic tie breaks.
pub fn select_operator(scores : Array[Double]) -> Int {
if scores.is_empty() {
return 0
}
let mut selected = 0
for index in 1.. scores[selected] {
selected = index
}
}
selected
}