Switch to log scale for diagnostics charts

Split the spectrum of the Hessian into its positive and negative parts
and plot them on the same logarithmic axis. The data zoom controls are
still linearly scaled, as discussed in ECharts issue #20927

  https://github.com/apache/echarts/issues/20927
This commit is contained in:
Aaron Fenyes 2025-06-25 12:57:11 -07:00
parent af28e885bb
commit 2688b76678

View file

@ -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<Option<f64>> {
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::<f64>]);
}
let scaled_loss_series = Line::new().data(
if scaled_loss.len() > 0 {
scaled_loss
} else {
vec![vec![Some(0.0), None::<f64>]]
}
);
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::<Vec<_>>()
.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::<f64>, None::<f64>]);
}
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::<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()
.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();
});
});