1use num_format::{Locale, ToFormattedString};
12use owo_colors::OwoColorize;
13use std::io::{self, Write};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::OnceLock;
16use std::time::Instant;
17use tracing::field::{Field, Visit};
18use tracing::{Event, Level, Subscriber};
19use tracing_subscriber::layer::Context;
20use tracing_subscriber::layer::SubscriberExt;
21use tracing_subscriber::util::SubscriberInitExt;
22use tracing_subscriber::{EnvFilter, Layer};
23
24static INIT: OnceLock<()> = OnceLock::new();
25static EPOCH: OnceLock<Instant> = OnceLock::new();
26static SOLVE_START_NANOS: AtomicU64 = AtomicU64::new(0);
27
28const VERSION: &str = env!("CARGO_PKG_VERSION");
30
31pub fn init() {
36 INIT.get_or_init(|| {
37 print_banner();
38
39 let filter = EnvFilter::builder()
40 .with_default_directive("solverforge_solver=info".parse().unwrap())
41 .from_env_lossy()
42 .add_directive("solverforge_dynamic=info".parse().unwrap());
43
44 let _ = tracing_subscriber::registry()
45 .with(filter)
46 .with(SolverConsoleLayer)
47 .try_init();
48 });
49}
50
51fn mark_solve_start() {
53 let epoch = EPOCH.get_or_init(Instant::now);
54 let nanos = epoch.elapsed().as_nanos() as u64;
55 SOLVE_START_NANOS.store(nanos, Ordering::Relaxed);
56}
57
58fn elapsed_secs() -> f64 {
60 let Some(epoch) = EPOCH.get() else {
61 return 0.0;
62 };
63 let start_nanos = SOLVE_START_NANOS.load(Ordering::Relaxed);
64 let now_nanos = epoch.elapsed().as_nanos() as u64;
65 (now_nanos - start_nanos) as f64 / 1_000_000_000.0
66}
67
68fn print_banner() {
69 let banner = r#"
70 ____ _ _____
71/ ___| ___ | |_ _____ _ __ | ___|__ _ __ __ _ ___
72\___ \ / _ \| \ \ / / _ \ '__|| |_ / _ \| '__/ _` |/ _ \
73 ___) | (_) | |\ V / __/ | | _| (_) | | | (_| | __/
74|____/ \___/|_| \_/ \___|_| |_| \___/|_| \__, |\___|
75 |___/
76"#;
77
78 let version_line = format!(
79 " v{} - Zero-Erasure Constraint Solver\n",
80 VERSION
81 );
82
83 let mut stdout = io::stdout().lock();
84 let _ = writeln!(stdout, "{}", banner.bright_cyan());
85 let _ = writeln!(stdout, "{}", version_line.bright_white().bold());
86 let _ = stdout.flush();
87}
88
89pub struct SolverConsoleLayer;
91
92impl<S: Subscriber> Layer<S> for SolverConsoleLayer {
93 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
94 let metadata = event.metadata();
95 let target = metadata.target();
96
97 if !target.starts_with("solverforge_solver")
99 && !target.starts_with("solverforge_dynamic")
100 && !target.starts_with("solverforge_py")
101 && !target.starts_with("solverforge::")
102 {
103 return;
104 }
105
106 let mut visitor = EventVisitor::default();
107 event.record(&mut visitor);
108
109 let level = *metadata.level();
110 let output = format_event(&visitor, level);
111 if !output.is_empty() {
112 let _ = writeln!(io::stdout(), "{}", output);
113 }
114 }
115}
116
117#[derive(Default)]
118struct EventVisitor {
119 event: Option<String>,
120 phase: Option<String>,
121 phase_index: Option<u64>,
122 steps: Option<u64>,
123 speed: Option<u64>,
124 score: Option<String>,
125 step: Option<u64>,
126 entity: Option<u64>,
127 accepted: Option<bool>,
128 duration_ms: Option<u64>,
129 entity_count: Option<u64>,
130 value_count: Option<u64>,
131 constraint_count: Option<u64>,
132 time_limit_secs: Option<u64>,
133 feasible: Option<bool>,
134 moves_speed: Option<u64>,
135 calc_speed: Option<u64>,
136 acceptance_rate: Option<String>,
137}
138
139impl Visit for EventVisitor {
140 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
141 let s = format!("{:?}", value);
142 match field.name() {
143 "event" => self.event = Some(s.trim_matches('"').to_string()),
144 "phase" => self.phase = Some(s.trim_matches('"').to_string()),
145 "score" => self.score = Some(s.trim_matches('"').to_string()),
146 _ => {}
147 }
148 }
149
150 fn record_u64(&mut self, field: &Field, value: u64) {
151 match field.name() {
152 "phase_index" => self.phase_index = Some(value),
153 "steps" => self.steps = Some(value),
154 "speed" => self.speed = Some(value),
155 "step" => self.step = Some(value),
156 "entity" => self.entity = Some(value),
157 "duration_ms" => self.duration_ms = Some(value),
158 "entity_count" => self.entity_count = Some(value),
159 "value_count" => self.value_count = Some(value),
160 "constraint_count" => self.constraint_count = Some(value),
161 "time_limit_secs" => self.time_limit_secs = Some(value),
162 "moves_speed" => self.moves_speed = Some(value),
163 "calc_speed" => self.calc_speed = Some(value),
164 _ => {}
165 }
166 }
167
168 fn record_i64(&mut self, field: &Field, value: i64) {
169 self.record_u64(field, value as u64);
170 }
171
172 fn record_bool(&mut self, field: &Field, value: bool) {
173 match field.name() {
174 "accepted" => self.accepted = Some(value),
175 "feasible" => self.feasible = Some(value),
176 _ => {}
177 }
178 }
179
180 fn record_str(&mut self, field: &Field, value: &str) {
181 match field.name() {
182 "event" => self.event = Some(value.to_string()),
183 "phase" => self.phase = Some(value.to_string()),
184 "score" => self.score = Some(value.to_string()),
185 "acceptance_rate" => self.acceptance_rate = Some(value.to_string()),
186 _ => {}
187 }
188 }
189}
190
191fn format_event(v: &EventVisitor, level: Level) -> String {
192 let event = v.event.as_deref().unwrap_or("");
193
194 match event {
195 "solve_start" => format_solve_start(v),
196 "solve_end" => format_solve_end(v),
197 "phase_start" => format_phase_start(v),
198 "phase_end" => format_phase_end(v),
199 "progress" => format_progress(v),
200 "step" => format_step(v, level),
201 _ => String::new(),
202 }
203}
204
205fn format_elapsed() -> String {
206 format!("{:>7.3}s", elapsed_secs())
207 .bright_black()
208 .to_string()
209}
210
211fn format_solve_start(v: &EventVisitor) -> String {
212 mark_solve_start();
213 let entities = v.entity_count.unwrap_or(0);
214 let values = v.value_count.unwrap_or(0);
215 let constraints = v.constraint_count.unwrap_or(0);
216 let time_limit = v.time_limit_secs.unwrap_or(0);
217 let scale = calculate_problem_scale(entities as usize, values as usize);
218
219 let mut output = format!(
220 "{} {} Solving │ {} entities │ {} values │ scale {}",
221 format_elapsed(),
222 "▶".bright_green().bold(),
223 entities.to_formatted_string(&Locale::en).bright_yellow(),
224 values.to_formatted_string(&Locale::en).bright_yellow(),
225 scale.bright_magenta()
226 );
227
228 if constraints > 0 {
229 output.push_str(&format!(
230 " │ {} constraints",
231 constraints.to_formatted_string(&Locale::en).bright_yellow()
232 ));
233 }
234
235 if time_limit > 0 {
236 output.push_str(&format!(
237 " │ {}s limit",
238 time_limit.to_formatted_string(&Locale::en).bright_yellow()
239 ));
240 }
241
242 output
243}
244
245fn format_solve_end(v: &EventVisitor) -> String {
246 let score = v.score.as_deref().unwrap_or("N/A");
247 let is_feasible = v
248 .feasible
249 .unwrap_or_else(|| !score.contains('-') || score.starts_with("0hard"));
250
251 let status = if is_feasible {
252 "FEASIBLE".bright_green().bold().to_string()
253 } else {
254 "INFEASIBLE".bright_red().bold().to_string()
255 };
256
257 let mut output = format!(
258 "{} {} Solving complete │ {} │ {}",
259 format_elapsed(),
260 "■".bright_cyan().bold(),
261 format_score(score),
262 status
263 );
264
265 output.push_str("\n\n");
267 output.push_str(
268 &"╔══════════════════════════════════════════════════════════╗"
269 .bright_cyan()
270 .to_string(),
271 );
272 output.push('\n');
273
274 let status_text = if is_feasible {
275 "FEASIBLE SOLUTION FOUND"
276 } else {
277 "INFEASIBLE (hard constraints violated)"
278 };
279 let inner_width: usize = 58;
280 let total_pad = inner_width.saturating_sub(status_text.len());
281 let left_pad = total_pad / 2;
282 let right_pad = total_pad - left_pad;
283 let status_colored = if is_feasible {
284 status_text.bright_green().bold().to_string()
285 } else {
286 status_text.bright_red().bold().to_string()
287 };
288 output.push_str(&format!(
289 "{}{}{}{}{}",
290 "║".bright_cyan(),
291 " ".repeat(left_pad),
292 status_colored,
293 " ".repeat(right_pad),
294 "║".bright_cyan()
295 ));
296 output.push('\n');
297
298 output.push_str(
299 &"╠══════════════════════════════════════════════════════════╣"
300 .bright_cyan()
301 .to_string(),
302 );
303 output.push('\n');
304
305 output.push_str(&format!(
306 "{} {:<18}{:>36} {}",
307 "║".bright_cyan(),
308 "Final Score:",
309 score,
310 "║".bright_cyan()
311 ));
312 output.push('\n');
313
314 output.push_str(
315 &"╚══════════════════════════════════════════════════════════╝"
316 .bright_cyan()
317 .to_string(),
318 );
319 output.push('\n');
320
321 output
322}
323
324fn format_phase_start(v: &EventVisitor) -> String {
325 let phase = v.phase.as_deref().unwrap_or("Unknown");
326
327 format!(
328 "{} {} {} started",
329 format_elapsed(),
330 "▶".bright_blue(),
331 phase.white().bold()
332 )
333}
334
335fn format_phase_end(v: &EventVisitor) -> String {
336 let phase = v.phase.as_deref().unwrap_or("Unknown");
337 let steps = v.steps.unwrap_or(0);
338 let moves_speed = v.moves_speed.unwrap_or(v.speed.unwrap_or(0));
339 let score = v.score.as_deref().unwrap_or("N/A");
340 let duration = v.duration_ms.unwrap_or(0);
341
342 let mut output = format!(
343 "{} {} {} ended │ {} │ {} steps │ {} moves/s",
344 format_elapsed(),
345 "◀".bright_blue(),
346 phase.white().bold(),
347 format_duration_ms(duration).yellow(),
348 steps.to_formatted_string(&Locale::en).white(),
349 moves_speed
350 .to_formatted_string(&Locale::en)
351 .bright_magenta()
352 .bold(),
353 );
354
355 if let Some(calc_speed) = v.calc_speed {
356 output.push_str(&format!(
357 " │ {} calcs/s",
358 calc_speed
359 .to_formatted_string(&Locale::en)
360 .bright_magenta()
361 .bold()
362 ));
363 }
364
365 if let Some(ref rate) = v.acceptance_rate {
366 output.push_str(&format!(" │ {} accepted", rate.bright_yellow()));
367 }
368
369 output.push_str(&format!(" │ {}", format_score(score)));
370
371 output
372}
373
374fn format_progress(v: &EventVisitor) -> String {
375 let steps = v.steps.unwrap_or(0);
376 let speed = v.speed.unwrap_or(0);
377 let score = v.score.as_deref().unwrap_or("N/A");
378
379 format!(
380 "{} {} {:>10} steps │ {:>12}/s │ {}",
381 format_elapsed(),
382 "⚡".bright_cyan(),
383 steps.to_formatted_string(&Locale::en).white(),
384 speed
385 .to_formatted_string(&Locale::en)
386 .bright_magenta()
387 .bold(),
388 format_score(score)
389 )
390}
391
392fn format_step(v: &EventVisitor, level: Level) -> String {
393 if level != Level::TRACE {
394 return String::new();
395 }
396
397 let step = v.step.unwrap_or(0);
398 let entity = v.entity.unwrap_or(0);
399 let score = v.score.as_deref().unwrap_or("N/A");
400 let accepted = v.accepted.unwrap_or(false);
401
402 let icon = if accepted {
403 "✓".bright_green().to_string()
404 } else {
405 "✗".bright_red().to_string()
406 };
407
408 format!(
409 "{} {} Step {:>10} │ Entity {:>6} │ {}",
410 format_elapsed(),
411 icon,
412 step.to_formatted_string(&Locale::en).bright_black(),
413 entity.to_formatted_string(&Locale::en).bright_black(),
414 format_score(score).bright_black()
415 )
416}
417
418fn format_duration_ms(ms: u64) -> String {
419 if ms < 1000 {
420 format!("{}ms", ms)
421 } else if ms < 60_000 {
422 format!("{:.2}s", ms as f64 / 1000.0)
423 } else {
424 let mins = ms / 60_000;
425 let secs = (ms % 60_000) / 1000;
426 format!("{}m {}s", mins, secs)
427 }
428}
429
430fn format_score(score: &str) -> String {
431 if score.contains("hard") {
432 let parts: Vec<&str> = score.split('/').collect();
433 if parts.len() == 2 {
434 let hard = parts[0].trim_end_matches("hard");
435 let soft = parts[1].trim_end_matches("soft");
436
437 let hard_num: f64 = hard.parse().unwrap_or(0.0);
438 let soft_num: f64 = soft.parse().unwrap_or(0.0);
439
440 let hard_str = if hard_num < 0.0 {
441 format!("{}hard", hard).bright_red().to_string()
442 } else {
443 format!("{}hard", hard).bright_green().to_string()
444 };
445
446 let soft_str = if soft_num < 0.0 {
447 format!("{}soft", soft).yellow().to_string()
448 } else if soft_num > 0.0 {
449 format!("{}soft", soft).bright_green().to_string()
450 } else {
451 format!("{}soft", soft).white().to_string()
452 };
453
454 return format!("{}/{}", hard_str, soft_str);
455 }
456 }
457
458 if let Ok(n) = score.parse::<i32>() {
459 if n < 0 {
460 return score.bright_red().to_string();
461 } else if n > 0 {
462 return score.bright_green().to_string();
463 }
464 }
465
466 score.white().to_string()
467}
468
469fn calculate_problem_scale(entity_count: usize, value_count: usize) -> String {
470 if entity_count == 0 || value_count == 0 {
471 return "0".to_string();
472 }
473
474 let log_scale = (entity_count as f64) * (value_count as f64).log10();
475 let exponent = log_scale.floor() as i32;
476 let mantissa = 10f64.powf(log_scale - exponent as f64);
477
478 format!("{:.3} x 10^{}", mantissa, exponent)
479}