feat: Engine diagnostics #92

Merged
glen merged 16 commits from Vectornaut/dyna3:diagnostics into main 2025-07-21 04:18:50 +00:00
Showing only changes of commit 2688b76678 - Show all commits

View file

@ -2,7 +2,7 @@ use charming::{
Chart, Chart,
WasmRenderer, WasmRenderer,
component::{Axis, DataZoom, Grid}, component::{Axis, DataZoom, Grid},
element::AxisType, element::{AxisType, Symbol},
series::{Line, Scatter}, series::{Line, Scatter},
}; };
use sycamore::prelude::*; use sycamore::prelude::*;
@ -50,6 +50,10 @@ fn RealizationStatus() -> View {
} }
} }
fn into_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
vec![Some(step as f64), Some(value)]
}
// the loss history from the last realization // the loss history from the last realization
#[component] #[component]
fn LossHistory() -> View { fn LossHistory() -> View {
@ -60,38 +64,35 @@ fn LossHistory() -> View {
on_mount(move || { on_mount(move || {
create_effect(move || { create_effect(move || {
// get the loss history // get the loss history
let scaled_loss = state.assembly.descent_history.with( let scaled_loss: Vec<_> = state.assembly.descent_history.with(
|history| history.scaled_loss.clone() |history| history.scaled_loss
.iter()
.enumerate()
.map(|(step, &loss)| (step, loss))
.map(into_time_point)
.collect()
); );
let step_cnt = scaled_loss.len();
// initialize the chart axes and series // initialize the chart axes and series
const MIN_INTERVAL: f64 = 0.01; let step_axis = Axis::new()
let mut step_axis = Axis::new()
.type_(AxisType::Category) .type_(AxisType::Category)
.boundary_gap(false); .boundary_gap(false);
let scaled_loss_axis = Axis::new() let scaled_loss_axis = Axis::new().type_(AxisType::Log);
.type_(AxisType::Value)
.min(0)
.min_interval(MIN_INTERVAL);
// load the chart data. when there's no history, we load the data // load the chart data. when there's no history, we load the data
// point (0, None) to clear the chart. it would feel more natural to // point (0, None) to clear the chart. it would feel more natural to
// load empty data vectors, but that turns out not to clear the // load empty data vectors, but that turns out not to clear the
// chart: it instead leads to previous data being re-used // chart: it instead leads to previous data being re-used
let mut scaled_loss_series = Line::new(); let scaled_loss_series = Line::new().data(
if step_cnt > 0 { if scaled_loss.len() > 0 {
step_axis = step_axis.data( scaled_loss
(0..step_cnt).map(|step| step.to_string()).collect() } else {
); vec![vec![Some(0.0), None::<f64>]]
scaled_loss_series = scaled_loss_series.data(scaled_loss); }
} else { );
step_axis = step_axis.data(vec![0.to_string()]);
scaled_loss_series = scaled_loss_series.data(vec![None::<f64>]);
}
let chart = Chart::new() let chart = Chart::new()
.animation(false) .animation(false)
.data_zoom(DataZoom::new().y_axis_index(0).right(40).start(0).end(100)) .data_zoom(DataZoom::new().y_axis_index(0).right(40))
.x_axis(step_axis) .x_axis(step_axis)
.y_axis(scaled_loss_axis) .y_axis(scaled_loss_axis)
.grid(Grid::new().top(20).right(80).bottom(30).left(60)) .grid(Grid::new().top(20).right(80).bottom(30).left(60))
@ -114,41 +115,73 @@ fn SpectrumHistory() -> View {
on_mount(move || { on_mount(move || {
create_effect(move || { create_effect(move || {
// get the spectrum of the Hessian at each step // get the spectrum of the Hessian at each step, split into its
let hess_eigvals = state.assembly.descent_history.with( // positive and negative parts. throw away eigenvalues that are
// close to zero
const ZERO_THRESHOLD: f64 = 1e-6;
let (
hess_eigvals_pos,
hess_eigvals_neg
): (Vec<_>, Vec<_>) = state.assembly.descent_history.with(
|history| history.hess_eigvals |history| history.hess_eigvals
.iter() .iter()
.enumerate() .enumerate()
.map( .map(
|(step, eigvals)| eigvals.iter().map( |(step, eigvals)| eigvals
move |val| vec![step as f64, *val] .iter()
) .filter(|&&val| val.abs() > ZERO_THRESHOLD)
.map(
move |&val| (step, val)
)
) )
.flatten() .flatten()
.collect::<Vec<_>>() .partition(|&(_, val)| val > 0.0)
); );
// initialize the chart axes and series // initialize the chart axes and series
let step_axis = Axis::new(); let step_axis = Axis::new()
let eigval_axis = Axis::new(); .type_(AxisType::Category)
.boundary_gap(false);
let eigval_axis = Axis::new().type_(AxisType::Log);
// load the chart data. when there's no history, we load the data // load the chart data. when there's no history, we load the data
// point (0, None) to clear the chart. it would feel more natural to // point (0, None) to clear the chart. it would feel more natural to
// load empty data vectors, but that turns out not to clear the // load empty data vectors, but that turns out not to clear the
// chart: it instead leads to previous data being re-used // chart: it instead leads to previous data being re-used
let mut eigval_series = Scatter::new().symbol_size(7); let eigval_series_pos = Scatter::new()
if hess_eigvals.len() > 0 { .symbol_size(4.5)
eigval_series = eigval_series.data(hess_eigvals); .data(
} else { if hess_eigvals_pos.len() > 0 {
eigval_series = eigval_series.data(vec![None::<f64>, None::<f64>]); hess_eigvals_pos
} .into_iter()
.map(into_time_point)
.collect()
} else {
vec![vec![Some(0.0), None::<f64>]]
}
);
let eigval_series_neg = Scatter::new()
.symbol(Symbol::Diamond)
.symbol_size(6.0)
.data(
if hess_eigvals_neg.len() > 0 {
hess_eigvals_neg
.into_iter()
.map(|(step, val)| (step, -val))
.map(into_time_point)
.collect()
} else {
vec![vec![Some(0.0), None::<f64>]]
}
);
let chart = Chart::new() let chart = Chart::new()
.animation(false) .animation(false)
.data_zoom(DataZoom::new().y_axis_index(0).right(40).start(0).end(100)) .data_zoom(DataZoom::new().y_axis_index(0).right(40))
.x_axis(step_axis) .x_axis(step_axis)
.y_axis(eigval_axis) .y_axis(eigval_axis)
.grid(Grid::new().top(20).right(80).bottom(30).left(60)) .grid(Grid::new().top(20).right(80).bottom(30).left(60))
.series(eigval_series); .series(eigval_series_pos)
.series(eigval_series_neg);
renderer.render(CONTAINER_ID, &chart).unwrap(); renderer.render(CONTAINER_ID, &chart).unwrap();
}); });
}); });