///|
/// A compact convergence summary for CLI and benchmark output.
pub struct SearchSummary {
instance_name : String
iterations : Int
seed_objective : Double
best_objective : Double
absolute_improvement : Double
relative_improvement : Double
accepted_moves : Int
improved_moves : Int
local_improvements : Int
evaluated_candidates : Int
feasible : Bool
} derive(Debug, ToJson)
///|
pub fn SearchSummary::instance_name(self : SearchSummary) -> String {
self.instance_name
}
///|
pub fn SearchSummary::iterations(self : SearchSummary) -> Int {
self.iterations
}
///|
pub fn SearchSummary::seed_objective(self : SearchSummary) -> Double {
self.seed_objective
}
///|
pub fn SearchSummary::best_objective(self : SearchSummary) -> Double {
self.best_objective
}
///|
pub fn SearchSummary::absolute_improvement(self : SearchSummary) -> Double {
self.absolute_improvement
}
///|
pub fn SearchSummary::relative_improvement(self : SearchSummary) -> Double {
self.relative_improvement
}
///|
pub fn SearchSummary::accepted_moves(self : SearchSummary) -> Int {
self.accepted_moves
}
///|
pub fn SearchSummary::improved_moves(self : SearchSummary) -> Int {
self.improved_moves
}
///|
pub fn SearchSummary::local_improvements(self : SearchSummary) -> Int {
self.local_improvements
}
///|
pub fn SearchSummary::evaluated_candidates(self : SearchSummary) -> Int {
self.evaluated_candidates
}
///|
pub fn SearchSummary::is_feasible(self : SearchSummary) -> Bool {
self.feasible
}
///|
/// Converts a solver result into stable, machine-readable metrics.
pub fn summarize_search(result : @core.SolveResult) -> SearchSummary {
let seed = result.stats.seed_objective
let best = result.stats.best_objective
let absolute = seed - best
let relative = if seed == 0.0 { 0.0 } else { absolute / seed }
{
instance_name: result.instance.instance_name(),
iterations: result.stats.iterations,
seed_objective: seed,
best_objective: best,
absolute_improvement: absolute,
relative_improvement: relative,
accepted_moves: result.stats.accepted_moves(),
improved_moves: result.stats.improved_moves(),
local_improvements: result.stats.local_improvements(),
evaluated_candidates: result.stats.evaluated_candidates(),
feasible: result.best_solution.is_feasible(),
}
}
///|
pub fn search_summary_text(summary : SearchSummary) -> String {
[
"Instance: \{summary.instance_name}",
"Iterations: \{summary.iterations}",
"Seed objective: \{summary.seed_objective}",
"Best objective: \{summary.best_objective}",
"Absolute improvement: \{summary.absolute_improvement}",
"Relative improvement: \{summary.relative_improvement}",
"Accepted moves: \{summary.accepted_moves}",
"Improved moves: \{summary.improved_moves}",
"Local improvements: \{summary.local_improvements}",
"Evaluated candidates: \{summary.evaluated_candidates}",
"Feasible: \{summary.feasible}",
].join("\n")
}
///|
/// Chooses the best result by objective, preferring feasible solutions on ties.
pub fn best_result_index(results : Array[@core.SolveResult]) -> Int? {
if results.is_empty() {
return None
}
let mut selected = 0
for index in 1.. Bool {
let first = solve(instance, config~)
let second = solve(instance, config~)
@core.solution_signature(instance, first.best_solution) ==
@core.solution_signature(instance, second.best_solution)
}
///|
/// Returns actionable quality flags without changing solver behavior.
pub fn quality_flags(
instance : @core.VrpInstance,
result : @core.SolveResult,
) -> Array[String] {
let flags : Array[String] = []
if !result.best_solution.feasible {
flags.push("best solution is infeasible")
}
if result.best_solution.unassigned_customers().length() > 0 {
flags.push("some customers are unassigned")
}
let validation = @core.validate_solution(instance, result.best_solution)
if !validation.is_valid() {
flags.append(@core.validation_messages(validation))
}
if result.stats.accepted_moves() == 0 && result.stats.iterations > 0 {
flags.push("no candidate move was accepted")
}
flags
}