diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index d283c51..0aa46c3 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -2,7 +2,7 @@ use charming::{ Chart, WasmRenderer, component::{Axis, DataZoom, Grid}, - element::AxisType, + element::{AxisType, Symbol}, series::{Line, Scatter}, }; use sycamore::prelude::*; @@ -50,6 +50,10 @@ fn RealizationStatus() -> View { } } +fn into_time_point((step, value): (usize, f64)) -> Vec> { + vec![Some(step as f64), Some(value)] +} + // the loss history from the last realization #[component] fn LossHistory() -> View { @@ -60,38 +64,35 @@ fn LossHistory() -> View { on_mount(move || { create_effect(move || { // get the loss history - let scaled_loss = state.assembly.descent_history.with( - |history| history.scaled_loss.clone() + let scaled_loss: Vec<_> = state.assembly.descent_history.with( + |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 - const MIN_INTERVAL: f64 = 0.01; - let mut step_axis = Axis::new() + let step_axis = Axis::new() .type_(AxisType::Category) .boundary_gap(false); - let scaled_loss_axis = Axis::new() - .type_(AxisType::Value) - .min(0) - .min_interval(MIN_INTERVAL); + let scaled_loss_axis = Axis::new().type_(AxisType::Log); // 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 // load empty data vectors, but that turns out not to clear the // chart: it instead leads to previous data being re-used - let mut scaled_loss_series = Line::new(); - if step_cnt > 0 { - step_axis = step_axis.data( - (0..step_cnt).map(|step| step.to_string()).collect() - ); - 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::]); - } + let scaled_loss_series = Line::new().data( + if scaled_loss.len() > 0 { + scaled_loss + } else { + vec![vec![Some(0.0), None::]] + } + ); let chart = Chart::new() .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) .y_axis(scaled_loss_axis) .grid(Grid::new().top(20).right(80).bottom(30).left(60)) @@ -114,41 +115,73 @@ fn SpectrumHistory() -> View { on_mount(move || { create_effect(move || { - // get the spectrum of the Hessian at each step - let hess_eigvals = state.assembly.descent_history.with( + // get the spectrum of the Hessian at each step, split into its + // 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 .iter() .enumerate() .map( - |(step, eigvals)| eigvals.iter().map( - move |val| vec![step as f64, *val] - ) + |(step, eigvals)| eigvals + .iter() + .filter(|&&val| val.abs() > ZERO_THRESHOLD) + .map( + move |&val| (step, val) + ) ) .flatten() - .collect::>() + .partition(|&(_, val)| val > 0.0) ); // initialize the chart axes and series - let step_axis = Axis::new(); - let eigval_axis = Axis::new(); + let step_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 // 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 // chart: it instead leads to previous data being re-used - let mut eigval_series = Scatter::new().symbol_size(7); - if hess_eigvals.len() > 0 { - eigval_series = eigval_series.data(hess_eigvals); - } else { - eigval_series = eigval_series.data(vec![None::, None::]); - } + let eigval_series_pos = Scatter::new() + .symbol_size(4.5) + .data( + if hess_eigvals_pos.len() > 0 { + hess_eigvals_pos + .into_iter() + .map(into_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + 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::]] + } + ); let chart = Chart::new() .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) .y_axis(eigval_axis) .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(); }); });