///|
/// Checked errors for the compact route exchange format.
pub(all) suberror RouteEncodingError {
EmptyRoute
InvalidCustomerId(String)
UnknownCustomerId(Int)
DuplicateCustomerId(Int)
} derive(Debug, Eq, ToJson)
///|
fn route_label_text(instance : VrpInstance, route : RoutePlan) -> String {
route.stops.map(fn(index) { "\{instance.label_of(index)}" }).join(",")
}
///|
/// Encodes routes as `id,id;id,id`, using external customer labels.
pub fn encode_routes(instance : VrpInstance, solution : Solution) -> String {
solution.routes.map(fn(route) { route_label_text(instance, route) }).join(";")
}
///|
/// Returns a stable route signature for equality/regression assertions.
pub fn solution_signature(
instance : VrpInstance,
solution : Solution,
) -> String {
let routes = encode_routes(instance, solution)
let unassigned = solution.unassigned
.map(fn(index) { "\{instance.label_of(index)}" })
.join(",")
"routes=\{routes};unassigned=\{unassigned}"
}
///|
fn parse_label(token : String) -> Int raise RouteEncodingError {
@strconv.from_str(token[:]) catch {
_ => raise RouteEncodingError::InvalidCustomerId(token)
}
}
///|
fn route_from_labels(
instance : VrpInstance,
text : String,
seen : Array[Int],
) -> Array[Int] raise RouteEncodingError {
let route : Array[Int] = []
for token in text.split(",") {
let cleaned = token.trim().to_owned()
if cleaned.is_empty() {
raise RouteEncodingError::EmptyRoute
}
let label = parse_label(cleaned)
match instance.index_of_label(label) {
None => raise RouteEncodingError::UnknownCustomerId(label)
Some(index) => {
if seen.contains(index) {
raise RouteEncodingError::DuplicateCustomerId(label)
}
seen.push(index)
route.push(index)
}
}
}
route
}
///|
/// Decodes the compact route format and assigns every omitted customer to the
/// returned solution's unassigned list.
pub fn decode_routes(
instance : VrpInstance,
text : String,
config : SolveConfig,
) -> Solution raise RouteEncodingError {
let routes : Array[Array[Int]] = []
let seen : Array[Int] = []
for route_text in text.split(";") {
let cleaned = route_text.trim().to_owned()
if cleaned.is_empty() {
continue
}
routes.push(route_from_labels(instance, cleaned, seen))
}
let unassigned = all_customer_indices(instance).filter(fn(index) {
!seen.contains(index)
})
build_solution(instance, routes, unassigned, config)
}
///|
/// Returns one line of route-level metrics for lightweight logging.
pub fn route_metrics_text(route : RoutePlan) -> String {
let metrics = route.metrics
"stops=\{route.stops.length()} load=\{metrics.load} distance=\{metrics.distance} duration=\{metrics.duration} waiting=\{metrics.waiting_time} feasible=\{metrics.feasible}"
}
///|
/// Returns a stable identity string for cache keys and benchmark manifests.
pub fn instance_signature(instance : VrpInstance) -> String {
let depot = instance.depot
"\{instance.name}|\{instance.variant.name()}|\{instance.metric.name()}|\{instance.customer_count()}|\{depot.x}|\{depot.y}|\{instance.vehicle.capacity}|\{instance.vehicle.fleet_size}"
}
///|
pub struct CostBreakdown {
distance : Double
capacity_penalty : Double
time_penalty : Double
duration_penalty : Double
unassigned_penalty : Double
objective : Double
} derive(Debug, ToJson)
///|
pub fn CostBreakdown::distance(self : CostBreakdown) -> Double {
self.distance
}
///|
pub fn CostBreakdown::capacity_penalty(self : CostBreakdown) -> Double {
self.capacity_penalty
}
///|
pub fn CostBreakdown::time_penalty(self : CostBreakdown) -> Double {
self.time_penalty
}
///|
pub fn CostBreakdown::duration_penalty(self : CostBreakdown) -> Double {
self.duration_penalty
}
///|
pub fn CostBreakdown::unassigned_penalty(self : CostBreakdown) -> Double {
self.unassigned_penalty
}
///|
pub fn CostBreakdown::objective(self : CostBreakdown) -> Double {
self.objective
}
///|
/// Explains the penalized objective without exposing the private formula.
pub fn Solution::cost_breakdown(
self : Solution,
config : SolveConfig,
) -> CostBreakdown {
let capacity_penalty = self.total_capacity_violation * config.capacity_penalty
let time_penalty = self.total_time_violation * config.time_penalty
let duration_penalty = self.total_duration_violation * config.duration_penalty
let unassigned_penalty = Double::from_int(self.unassigned.length()) *
config.unassigned_penalty
{
distance: self.total_distance,
capacity_penalty,
time_penalty,
duration_penalty,
unassigned_penalty,
objective: self.objective,
}
}