///|
/// Structural and feasibility diagnostics for instances and solutions.
pub(all) enum ValidationIssue {
InvalidVehicleCapacity
InvalidFleetSize
InvalidRouteDuration
InvalidDepotWindow
InvalidCustomerWindow(Int)
NegativeDemand(Int)
InvalidCoordinate(Int)
DuplicateExternalId(Int)
InvalidDistanceMatrix
TotalDemandExceedsFleet
TooManyRoutes
InvalidRouteCustomer(Int)
DuplicateAssignment(Int)
AssignmentOverlap(Int)
MissingAssignment(Int)
DuplicateUnassignment(Int)
} derive(Debug, Eq, ToJson)
///|
pub struct ValidationReport {
valid : Bool
issues : Array[ValidationIssue]
} derive(Debug, ToJson)
///|
fn ValidationReport::from_issues(
issues : Array[ValidationIssue],
) -> ValidationReport {
{ valid: issues.is_empty(), issues }
}
///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
self.valid
}
///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
self.issues.length()
}
///|
pub fn ValidationReport::issues(
self : ValidationReport,
) -> Array[ValidationIssue] {
self.issues.copy()
}
///|
pub fn ValidationReport::has_issue(
self : ValidationReport,
issue : ValidationIssue,
) -> Bool {
self.issues.contains(issue)
}
///|
fn is_bad_number(value : Double) -> Bool {
value.is_nan() || value.is_inf()
}
///|
fn append_once(
issues : Array[ValidationIssue],
issue : ValidationIssue,
) -> Unit {
if !issues.contains(issue) {
issues.push(issue)
}
}
///|
fn validate_distance_matrix(
instance : VrpInstance,
issues : Array[ValidationIssue],
) -> Unit {
let expected = instance.customer_count() + 1
if instance.distance_matrix.length() != expected {
append_once(issues, ValidationIssue::InvalidDistanceMatrix)
return
}
for row in instance.distance_matrix {
if row.length() != expected {
append_once(issues, ValidationIssue::InvalidDistanceMatrix)
} else {
for value in row {
if is_bad_number(value) || value < 0.0 {
append_once(issues, ValidationIssue::InvalidDistanceMatrix)
}
}
}
}
}
///|
/// Checks data invariants without running a solver.
pub fn validate_instance(instance : VrpInstance) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if instance.vehicle.capacity <= 0.0 ||
is_bad_number(instance.vehicle.capacity) {
issues.push(ValidationIssue::InvalidVehicleCapacity)
}
if instance.vehicle.fleet_size <= 0 {
issues.push(ValidationIssue::InvalidFleetSize)
}
if instance.vehicle.max_route_duration <= 0.0 ||
is_bad_number(instance.vehicle.max_route_duration) {
issues.push(ValidationIssue::InvalidRouteDuration)
}
if instance.depot.ready_time > instance.depot.due_time ||
is_bad_number(instance.depot.ready_time) ||
is_bad_number(instance.depot.due_time) {
issues.push(ValidationIssue::InvalidDepotWindow)
}
if is_bad_number(instance.depot.x) || is_bad_number(instance.depot.y) {
issues.push(ValidationIssue::InvalidCoordinate(instance.depot_index()))
}
let identifiers : Array[Int] = []
let mut total_demand = 0.0
for index, customer in instance.customers {
if identifiers.contains(customer.external_id) {
append_once(
issues,
ValidationIssue::DuplicateExternalId(customer.external_id),
)
} else {
identifiers.push(customer.external_id)
}
if customer.demand < 0.0 || is_bad_number(customer.demand) {
append_once(issues, ValidationIssue::NegativeDemand(index))
}
total_demand += customer.demand
if customer.ready_time > customer.due_time ||
is_bad_number(customer.ready_time) ||
is_bad_number(customer.due_time) ||
customer.service_time < 0.0 ||
is_bad_number(customer.service_time) {
append_once(issues, ValidationIssue::InvalidCustomerWindow(index))
}
if is_bad_number(customer.x) || is_bad_number(customer.y) {
append_once(issues, ValidationIssue::InvalidCoordinate(index))
}
}
if total_demand >
instance.vehicle.capacity * Double::from_int(instance.vehicle.fleet_size) {
append_once(issues, ValidationIssue::TotalDemandExceedsFleet)
}
validate_distance_matrix(instance, issues)
ValidationReport::from_issues(issues)
}
///|
/// Checks that every customer is assigned exactly once or explicitly unassigned.
pub fn validate_solution(
instance : VrpInstance,
solution : Solution,
) -> ValidationReport {
let issues : Array[ValidationIssue] = []
let customer_count = instance.customer_count()
let assignments = Array::make(customer_count, 0)
for route in solution.routes {
for customer_index in route.stops {
if customer_index < 0 || customer_index >= customer_count {
append_once(
issues,
ValidationIssue::InvalidRouteCustomer(customer_index),
)
} else {
assignments[customer_index] += 1
if assignments[customer_index] > 1 {
append_once(
issues,
ValidationIssue::DuplicateAssignment(customer_index),
)
}
}
}
}
if solution.routes.length() > instance.vehicle.fleet_size {
append_once(issues, ValidationIssue::TooManyRoutes)
}
let unassigned_seen : Array[Int] = []
for customer_index in solution.unassigned {
if customer_index < 0 || customer_index >= customer_count {
append_once(issues, ValidationIssue::InvalidRouteCustomer(customer_index))
} else if unassigned_seen.contains(customer_index) {
append_once(
issues,
ValidationIssue::DuplicateUnassignment(customer_index),
)
} else {
unassigned_seen.push(customer_index)
if assignments[customer_index] > 0 {
append_once(issues, ValidationIssue::AssignmentOverlap(customer_index))
}
}
}
for customer_index in 0.. Array[String] {
report.issues.map(fn(issue) {
match issue {
ValidationIssue::InvalidVehicleCapacity =>
"vehicle capacity must be positive"
ValidationIssue::InvalidFleetSize => "fleet size must be positive"
ValidationIssue::InvalidRouteDuration =>
"maximum route duration must be positive"
ValidationIssue::InvalidDepotWindow => "depot time window is invalid"
ValidationIssue::InvalidCustomerWindow(index) =>
"customer \{index} has an invalid time or service window"
ValidationIssue::NegativeDemand(index) =>
"customer \{index} has negative demand"
ValidationIssue::InvalidCoordinate(index) =>
"node \{index} has a non-finite coordinate"
ValidationIssue::DuplicateExternalId(label) =>
"external customer id \{label} is duplicated"
ValidationIssue::InvalidDistanceMatrix => "distance matrix is invalid"
ValidationIssue::TotalDemandExceedsFleet =>
"total demand exceeds the available fleet capacity"
ValidationIssue::TooManyRoutes =>
"solution uses more routes than the fleet"
ValidationIssue::InvalidRouteCustomer(index) =>
"route references customer index \{index}"
ValidationIssue::DuplicateAssignment(index) =>
"customer \{index} appears in more than one route position"
ValidationIssue::AssignmentOverlap(index) =>
"customer \{index} is both assigned and unassigned"
ValidationIssue::MissingAssignment(index) =>
"customer \{index} is neither assigned nor unassigned"
ValidationIssue::DuplicateUnassignment(index) =>
"customer \{index} is listed as unassigned more than once"
}
})
}