use crate::state::{AppState, EvoState};
use crate::widgets::panels::MetricLineChartWidget;
use crate::widgets::{AppWidget, FnWidget, MetricDetailPanelWidget, Panel, TabComponent};
use radiate_engines::Chromosome;
use radiate_engines::stats::fmt_duration;
use ratatui::prelude::*;
use ratatui::style::{Color, Style, Stylize};
use ratatui::widgets::{Paragraph, Row, Table};
pub struct EngineStatusPanelWidget;
impl EngineStatusPanelWidget {
pub fn new() -> Self {
Self
}
}
impl<C: Chromosome> AppWidget<C> for EngineStatusPanelWidget {
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<C>) {
let metrics = &state.evo.metrics;
let elapsed = metrics
.time()
.and_then(|m| m.times().map(|t| t.sum()))
.map(fmt_duration)
.unwrap_or_else(|| "00:00:00.000".to_string());
let rows = if state.evo.pareto.objective.is_single() {
get_single_objective_summaries(&state.evo)
} else {
get_multi_objective_summaries(&state.evo)
};
let mut title = vec![
"Gen ".fg(Color::Gray).bold(),
format!("{}", state.evo.index).fg(Color::LightGreen),
];
if state.evo.pareto.objective.is_single() {
let current = state.evo.score.as_f32();
let best = state.evo.best_score.as_f32();
let at_peak = (current - best).abs() < f32::EPSILON || best == 0.0;
title.push(" | Score ".fg(Color::Gray).bold());
title.push(format!("{:.4}", current).fg(if at_peak {
Color::LightGreen
} else {
Color::Yellow
}));
if !at_peak {
title.push(" / ".fg(Color::Gray));
title.push(format!("{:.4} ", best).fg(Color::LightGreen));
} else {
title.push(" ".into());
}
} else {
title.push(" | MOGA ".fg(Color::Gray).bold());
}
title.push("| Time ".fg(Color::Gray).bold());
title.push(elapsed.clone().fg(Color::LightGreen));
let engine_table = Table::default()
.rows(crate::styles::striped_rows(rows))
.widths([Constraint::Fill(1), Constraint::Fill(1)]);
let engine_state = if state.run.engine {
if state.run.paused {
" Paused ".fg(Color::Yellow).bold()
} else {
" Running ".fg(Color::LightGreen).bold()
}
} else {
" Complete ".fg(Color::Red).bold()
};
Panel::new(FnWidget::new(|area, buf| {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(15), Constraint::Fill(1)])
.split(area);
Paragraph::new(Line::from(title).centered()).render(layout[0], buf);
Widget::render(engine_table, layout[1], buf);
}))
.titled(Line::from(engine_state).alignment(Alignment::Center))
.render(area, buf);
}
}
pub struct MetricModalWidget;
impl<C: Chromosome> AppWidget<C> for MetricModalWidget {
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<C>) {
let index = state.chart_view_index();
let tab_labels = state
.selected_metric_views()
.iter()
.map(|v| {
Span::styled(
format!(" {} ", v.label()),
Style::default().fg(Color::White),
)
})
.collect::<Vec<Span<'static>>>();
let [left, right] =
Layout::horizontal([Constraint::Percentage(25), Constraint::Fill(1)]).areas(area);
MetricDetailPanelWidget.render(left, buf, state);
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Fill(1)])
.split(right);
Panel::new(FnWidget::new(move |area, buf| {
TabComponent::new(tab_labels)
.select(index)
.render(area, buf);
}))
.render_inside_block(true)
.render(areas[0], buf);
MetricLineChartWidget::default()
.with_show_bottom_options(false)
.with_show_x_axis(true)
.render(areas[1], buf, state);
}
}
fn get_multi_objective_summaries<C: Chromosome>(evo: &EvoState<C>) -> Vec<Row<'static>> {
let metrics = &evo.metrics;
let diversity = metrics.diversity_ratio().map(|m| m.mean()).unwrap_or(0.0);
let diversity_last = metrics
.diversity_ratio()
.map(|m| m.last_value())
.unwrap_or(diversity);
let carryover = metrics.carryover_rate().map(|m| m.mean()).unwrap_or(0.0);
let carryover_last = metrics
.carryover_rate()
.map(|m| m.last_value())
.unwrap_or(carryover);
let survivor_count = metrics.survivor_count().map(|m| m.mean()).unwrap_or(0.0);
let new_children = metrics.new_children().map(|m| m.mean()).unwrap_or(0.0);
let front_size = metrics.front_size().map(|m| m.mean()).unwrap_or(0.0);
let front_entropy = metrics.front_entropy().map(|m| m.mean()).unwrap_or(0.0);
let front_entropy_last = metrics
.front_entropy()
.map(|m| m.last_value())
.unwrap_or(front_entropy);
let front_additions = metrics
.front_additions()
.map(|m| m.last_value() as usize)
.unwrap_or(0);
let front_removals = metrics
.front_removals()
.map(|m| m.last_value() as usize)
.unwrap_or(0);
let species_count = metrics
.species_count()
.map(|m| m.last_value() as usize)
.unwrap_or(0);
let mut rows = vec![
Row::new(vec![
"Front Additions".bold(),
Span::styled(
format!("{}", front_additions),
Style::default().fg(if front_additions > 0 {
crate::styles::TREND_UP_COLOR
} else {
Color::DarkGray
}),
),
]),
Row::new(vec![
"Front Removals".bold(),
Span::styled(
format!("{}", front_removals),
Style::default().fg(if front_removals > 0 {
crate::styles::TREND_DOWN_COLOR
} else {
Color::DarkGray
}),
),
]),
Row::new(vec![
"Diversity".bold(),
Span::styled(
format!(
"{} {:.2}%",
crate::styles::trend_symbol(diversity_last, diversity),
diversity * 100.0
),
Style::default().fg(crate::styles::sentiment_color(diversity, 0.1, 0.3)),
),
]),
Row::new(vec![
"Carryover".bold(),
Span::styled(
format!(
"{} {:.2}%",
crate::styles::trend_symbol(carryover, carryover_last),
carryover * 100.0
),
Style::default().fg(crate::styles::sentiment_color(1.0 - carryover, 0.2, 0.5)),
),
]),
Row::new(vec![
"Front Size".bold(),
format!("{:.2}", front_size).into(),
]),
Row::new(vec![
"Front Entropy".bold(),
Span::styled(
format!("{:.4}", front_entropy),
Style::default().fg(crate::styles::trend_color(
front_entropy_last,
front_entropy,
)),
),
]),
Row::new(vec![
"Survivor / Gen.".bold(),
format!("{:.2}", survivor_count).into(),
]),
Row::new(vec![
"Children / Gen.".bold(),
format!("{:.2}", new_children).into(),
]),
];
if species_count > 0 {
rows.push(Row::new(vec![
"Species".bold(),
format!("{}", species_count).into(),
]));
}
rows
}
fn get_single_objective_summaries<C: Chromosome>(evo: &EvoState<C>) -> Vec<Row<'static>> {
let metrics = &evo.metrics;
let diversity = metrics.diversity_ratio().map(|m| m.mean()).unwrap_or(0.0);
let diversity_last = metrics
.diversity_ratio()
.map(|m| m.last_value())
.unwrap_or(diversity);
let carryover = metrics.carryover_rate().map(|m| m.mean()).unwrap_or(0.0);
let carryover_last = metrics
.carryover_rate()
.map(|m| m.last_value())
.unwrap_or(carryover);
let improvements = metrics.improvements().map(|m| m.sum()).unwrap_or(0.0) as usize;
let survivor_count = metrics.survivor_count().map(|m| m.mean()).unwrap_or(0.0);
let new_children = metrics.new_children().map(|m| m.mean()).unwrap_or(0.0);
let avg_score = metrics.score().map(|m| m.mean()).unwrap_or(0.0);
let stagnation = metrics
.stagnation_count()
.map(|m| m.last_value() as usize)
.unwrap_or(0);
let species_count = metrics
.species_count()
.map(|m| m.last_value() as usize)
.unwrap_or(0);
let mut rows = vec![
Row::new(vec![
"Improvements".bold(),
Span::styled(
improvements.to_string(),
Style::default().fg(if improvements == 0 {
Color::DarkGray
} else {
crate::styles::stagnation_dark_color(stagnation)
}),
),
]),
Row::new(vec![
"Stagnation".bold(),
Span::styled(
format!("{}", stagnation),
Style::default().fg(crate::styles::stagnation_dark_color(stagnation)),
),
]),
Row::new(vec!["Avg Score".bold(), format!("{:.6}", avg_score).into()]),
Row::new(vec![
"Diversity".bold(),
Span::styled(
format!(
"{} {:.2}%",
crate::styles::trend_symbol(diversity_last, diversity),
diversity * 100.0
),
Style::default().fg(crate::styles::sentiment_color(diversity, 0.1, 0.3)),
),
]),
Row::new(vec![
"Carryover".bold(),
Span::styled(
format!(
"{} {:.2}%",
crate::styles::trend_symbol(carryover, carryover_last),
carryover * 100.0
),
Style::default().fg(crate::styles::sentiment_color(1.0 - carryover, 0.2, 0.5)),
),
]),
Row::new(vec![
"Survivor / Gen.".bold(),
format!("{:.2}", survivor_count).into(),
]),
Row::new(vec![
"Children / Gen.".bold(),
format!("{:.2}", new_children).into(),
]),
];
if species_count > 0 {
rows.push(Row::new(vec![
"Species".bold(),
format!("{}", species_count).into(),
]));
}
rows
}