| |
|
|
| use num_format::{Locale, ToFormattedString}; |
| use owo_colors::OwoColorize; |
| use std::time::{Duration, Instant}; |
|
|
| |
| pub fn print_banner() { |
| let banner = r#" |
| ____ _ _____ |
| / ___| ___ | |_ _____ _ __| ___|__ _ __ __ _ ___ |
| \___ \ / _ \| \ \ / / _ \ '__| |_ / _ \| '__/ _` |/ _ \ |
| ___) | (_) | |\ V / __/ | | _| (_) | | | (_| | __/ |
| |____/ \___/|_| \_/ \___|_| |_| \___/|_| \__, |\___| |
| |___/ |
| "#; |
| println!("{}", banner.cyan().bold()); |
| println!( |
| " {} {}\n", |
| format!("v{}", env!("CARGO_PKG_VERSION")).bright_black(), |
| "Employee Scheduling".bright_cyan() |
| ); |
| } |
|
|
| |
| pub fn print_solving_started( |
| time_spent_ms: u64, |
| best_score: &str, |
| entity_count: usize, |
| variable_count: usize, |
| value_count: usize, |
| ) { |
| println!( |
| "{} {} {} time spent ({}), best score ({}), random ({})", |
| timestamp().bright_black(), |
| "INFO".bright_green(), |
| "[Solver]".bright_cyan(), |
| format!("{}ms", time_spent_ms).yellow(), |
| format_score(best_score), |
| "StdRng".white() |
| ); |
|
|
| |
| let scale = calculate_problem_scale(entity_count, value_count); |
| println!( |
| "{} {} {} entity count ({}), variable count ({}), value count ({}), problem scale ({})", |
| timestamp().bright_black(), |
| "INFO".bright_green(), |
| "[Solver]".bright_cyan(), |
| entity_count.to_formatted_string(&Locale::en).bright_yellow(), |
| variable_count.to_formatted_string(&Locale::en).bright_yellow(), |
| value_count.to_formatted_string(&Locale::en).bright_yellow(), |
| scale.bright_magenta() |
| ); |
| } |
|
|
| |
| pub fn print_phase_start(phase_name: &str, phase_index: usize) { |
| println!( |
| "{} {} {} {} phase ({}) started", |
| timestamp().bright_black(), |
| "INFO".bright_green(), |
| format!("[{}]", phase_name).bright_cyan(), |
| phase_name.white().bold(), |
| phase_index.to_string().yellow() |
| ); |
| } |
|
|
| |
| pub fn print_phase_end( |
| phase_name: &str, |
| phase_index: usize, |
| duration: Duration, |
| steps_accepted: u64, |
| moves_evaluated: u64, |
| best_score: &str, |
| ) { |
| let moves_per_sec = if duration.as_secs_f64() > 0.0 { |
| (moves_evaluated as f64 / duration.as_secs_f64()) as u64 |
| } else { |
| 0 |
| }; |
| let acceptance_rate = if moves_evaluated > 0 { |
| (steps_accepted as f64 / moves_evaluated as f64) * 100.0 |
| } else { |
| 0.0 |
| }; |
|
|
| println!( |
| "{} {} {} {} phase ({}) ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), step total ({}, {:.1}% accepted)", |
| timestamp().bright_black(), |
| "INFO".bright_green(), |
| format!("[{}]", phase_name).bright_cyan(), |
| phase_name.white().bold(), |
| phase_index.to_string().yellow(), |
| format_duration(duration).yellow(), |
| format_score(best_score), |
| moves_per_sec.to_formatted_string(&Locale::en).bright_magenta().bold(), |
| steps_accepted.to_formatted_string(&Locale::en).white(), |
| acceptance_rate |
| ); |
| } |
|
|
| |
| pub fn print_step_progress( |
| step: u64, |
| elapsed: Duration, |
| moves_evaluated: u64, |
| score: &str, |
| ) { |
| let moves_per_sec = if elapsed.as_secs_f64() > 0.0 { |
| (moves_evaluated as f64 / elapsed.as_secs_f64()) as u64 |
| } else { |
| 0 |
| }; |
|
|
| println!( |
| " {} Step {:>7} │ {} │ {}/sec │ {}", |
| "→".bright_blue(), |
| step.to_formatted_string(&Locale::en).white(), |
| format!("{:>6}", format_duration(elapsed)).bright_black(), |
| format!("{:>8}", moves_per_sec.to_formatted_string(&Locale::en)).bright_magenta().bold(), |
| format_score(score) |
| ); |
| } |
|
|
| |
| pub fn print_solving_ended( |
| total_duration: Duration, |
| total_moves: u64, |
| phase_count: usize, |
| final_score: &str, |
| is_feasible: bool, |
| ) { |
| let moves_per_sec = if total_duration.as_secs_f64() > 0.0 { |
| (total_moves as f64 / total_duration.as_secs_f64()) as u64 |
| } else { |
| 0 |
| }; |
|
|
| println!( |
| "{} {} {} Solving ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), phase total ({})", |
| timestamp().bright_black(), |
| "INFO".bright_green(), |
| "[Solver]".bright_cyan(), |
| format_duration(total_duration).yellow(), |
| format_score(final_score), |
| moves_per_sec.to_formatted_string(&Locale::en).bright_magenta().bold(), |
| phase_count.to_string().white() |
| ); |
|
|
| |
| println!(); |
| println!("{}", "╔══════════════════════════════════════════════════════════╗".bright_cyan()); |
|
|
| let status_text = if is_feasible { |
| "✓ FEASIBLE SOLUTION FOUND" |
| } else { |
| "✗ INFEASIBLE (hard constraints violated)" |
| }; |
| let status_colored = if is_feasible { |
| status_text.bright_green().bold().to_string() |
| } else { |
| status_text.bright_red().bold().to_string() |
| }; |
| let status_padding = 56 - status_text.chars().count(); |
| let left_pad = status_padding / 2; |
| let right_pad = status_padding - left_pad; |
| println!( |
| "{}{}{}{}{}", |
| "║".bright_cyan(), |
| " ".repeat(left_pad), |
| status_colored, |
| " ".repeat(right_pad), |
| "║".bright_cyan() |
| ); |
|
|
| println!("{}", "╠══════════════════════════════════════════════════════════╣".bright_cyan()); |
|
|
| let score_str = final_score; |
| println!( |
| "{} {:<18}{:>36} {}", |
| "║".bright_cyan(), |
| "Final Score:", |
| score_str, |
| "║".bright_cyan() |
| ); |
|
|
| let time_str = format!("{:.2}s", total_duration.as_secs_f64()); |
| println!( |
| "{} {:<18}{:>36} {}", |
| "║".bright_cyan(), |
| "Solving Time:", |
| time_str, |
| "║".bright_cyan() |
| ); |
|
|
| let speed_str = format!("{}/sec", moves_per_sec.to_formatted_string(&Locale::en)); |
| println!( |
| "{} {:<18}{:>36} {}", |
| "║".bright_cyan(), |
| "Move Speed:", |
| speed_str, |
| "║".bright_cyan() |
| ); |
|
|
| println!("{}", "╚══════════════════════════════════════════════════════════╝".bright_cyan()); |
| println!(); |
| } |
|
|
| |
| pub fn print_constraint_analysis(constraints: &[(String, String, usize)]) { |
| println!( |
| "{} {} {} Constraint Analysis:", |
| timestamp().bright_black(), |
| "INFO".bright_green(), |
| "[Solver]".bright_cyan() |
| ); |
|
|
| for (name, score, match_count) in constraints { |
| let status = if score.starts_with("0") || score == "0hard/0soft" { |
| "✓".bright_green().to_string() |
| } else if score.contains("hard") && !score.starts_with("0hard") { |
| "✗".bright_red().to_string() |
| } else { |
| "○".yellow().to_string() |
| }; |
|
|
| println!( |
| " {} {:<40} {:>15} ({} matches)", |
| status, |
| name.white(), |
| format_score(score), |
| match_count.to_formatted_string(&Locale::en).bright_black() |
| ); |
| } |
| println!(); |
| } |
|
|
| |
| pub fn print_config(shifts: usize, employees: usize) { |
| println!( |
| "{} {} {} Solver configuration: shifts ({}), employees ({})", |
| timestamp().bright_black(), |
| "INFO".bright_green(), |
| "[Solver]".bright_cyan(), |
| shifts.to_formatted_string(&Locale::en).bright_yellow(), |
| employees.to_formatted_string(&Locale::en).bright_yellow() |
| ); |
| } |
|
|
| |
| fn format_duration(d: Duration) -> String { |
| let total_ms = d.as_millis(); |
| if total_ms < 1000 { |
| format!("{}ms", total_ms) |
| } else if total_ms < 60_000 { |
| format!("{:.2}s", d.as_secs_f64()) |
| } else { |
| let mins = total_ms / 60_000; |
| let secs = (total_ms % 60_000) / 1000; |
| format!("{}m {}s", mins, secs) |
| } |
| } |
|
|
| |
| fn format_score(score: &str) -> String { |
| |
| if score.contains("hard") { |
| let parts: Vec<&str> = score.split('/').collect(); |
| if parts.len() == 2 { |
| let hard = parts[0].trim_end_matches("hard"); |
| let soft = parts[1].trim_end_matches("soft"); |
|
|
| let hard_num: f64 = hard.parse().unwrap_or(0.0); |
| let soft_num: f64 = soft.parse().unwrap_or(0.0); |
|
|
| let hard_str = if hard_num < 0.0 { |
| format!("{}hard", hard).bright_red().to_string() |
| } else { |
| format!("{}hard", hard).bright_green().to_string() |
| }; |
|
|
| let soft_str = if soft_num < 0.0 { |
| format!("{}soft", soft).yellow().to_string() |
| } else if soft_num > 0.0 { |
| format!("{}soft", soft).bright_green().to_string() |
| } else { |
| format!("{}soft", soft).white().to_string() |
| }; |
|
|
| return format!("{}/{}", hard_str, soft_str); |
| } |
| } |
|
|
| |
| if let Ok(n) = score.parse::<i32>() { |
| if n < 0 { |
| return score.bright_red().to_string(); |
| } else if n > 0 { |
| return score.bright_green().to_string(); |
| } |
| } |
|
|
| score.white().to_string() |
| } |
|
|
| |
| fn timestamp() -> String { |
| std::time::SystemTime::now() |
| .duration_since(std::time::UNIX_EPOCH) |
| .map(|d| { |
| let secs = d.as_secs(); |
| let millis = d.subsec_millis(); |
| format!("{}.{:03}", secs, millis) |
| }) |
| .unwrap_or_else(|_| "0.000".to_string()) |
| } |
|
|
| |
| fn calculate_problem_scale(entity_count: usize, value_count: usize) -> String { |
| if entity_count == 0 || value_count == 0 { |
| return "0".to_string(); |
| } |
|
|
| |
| let log_scale = (entity_count as f64) * (value_count as f64).log10(); |
| let exponent = log_scale.floor() as i32; |
| let mantissa = 10f64.powf(log_scale - exponent as f64); |
|
|
| format!("{:.3} × 10^{}", mantissa, exponent) |
| } |
|
|
| |
| pub struct PhaseTimer { |
| start: Instant, |
| phase_name: String, |
| phase_index: usize, |
| steps_accepted: u64, |
| moves_evaluated: u64, |
| last_score: String, |
| } |
|
|
| impl PhaseTimer { |
| pub fn start(phase_name: impl Into<String>, phase_index: usize) -> Self { |
| let name = phase_name.into(); |
| print_phase_start(&name, phase_index); |
| Self { |
| start: Instant::now(), |
| phase_name: name, |
| phase_index, |
| steps_accepted: 0, |
| moves_evaluated: 0, |
| last_score: String::new(), |
| } |
| } |
|
|
| pub fn record_accepted(&mut self, score: &str) { |
| self.steps_accepted += 1; |
| self.last_score = score.to_string(); |
| } |
|
|
| pub fn record_move(&mut self) { |
| self.moves_evaluated += 1; |
| } |
|
|
| pub fn elapsed(&self) -> Duration { |
| self.start.elapsed() |
| } |
|
|
| pub fn moves_evaluated(&self) -> u64 { |
| self.moves_evaluated |
| } |
|
|
| pub fn finish(self) { |
| print_phase_end( |
| &self.phase_name, |
| self.phase_index, |
| self.start.elapsed(), |
| self.steps_accepted, |
| self.moves_evaluated, |
| &self.last_score, |
| ); |
| } |
|
|
| pub fn steps_accepted(&self) -> u64 { |
| self.steps_accepted |
| } |
| } |
|
|