From 0be7448e2475b990572ed3c7989eaa52eabf980b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 11 Jun 2025 01:21:18 -0700 Subject: [PATCH] Add a spectrum history panel This introduces a framework for adding more diagnostics panels. --- app-proto/main.css | 15 ++++- app-proto/src/diagnostics.rs | 114 ++++++++++++++++++++++++++++++++--- app-proto/src/engine.rs | 9 +-- 3 files changed, 124 insertions(+), 14 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 14276dd..7981285 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -182,14 +182,23 @@ details[open]:has(li) .element-switch::after { margin: 10px; } +#diagnostics-bar { + display: flex; +} + #realization-status { display: flex; + flex-grow: 1; } #realization-status .status { margin-right: 4px; } +#realization-status :not(.status) { + flex-grow: 1; +} + #realization-status .status::after { content: '✓'; } @@ -198,8 +207,12 @@ details[open]:has(li) .element-switch::after { content: '⚠'; } -#loss-history { +.diagnostics-panel { margin-top: 10px; + min-height: 180px; +} + +.diagnostics-chart { background-color: var(--display-background); border: 1px solid var(--border); border-radius: 8px; diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index a73a793..b90989f 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -3,16 +3,28 @@ use charming::{ WasmRenderer, component::{Axis, Grid}, element::AxisType, - series::Line, - theme::Theme + series::{Line, Scatter}, }; use sycamore::prelude::*; use crate::AppState; +#[derive(Clone)] +struct DiagnosticsState { + active_tab: Signal +} + +impl DiagnosticsState { + fn new(initial_tab: String) -> DiagnosticsState { + DiagnosticsState { + active_tab: create_signal(initial_tab) + } + } +} + // a realization status indicator #[component] -pub fn RealizationStatus() -> View { +fn RealizationStatus() -> View { let state = use_context::(); let realization_status = state.assembly.realization_status; view! { @@ -38,12 +50,12 @@ pub fn RealizationStatus() -> View { } } -// a plot of the loss history from the last realization +// the loss history from the last realization #[component] -pub fn LossHistory() -> View { +fn LossHistory() -> View { const CONTAINER_ID: &str = "loss-history"; let state = use_context::(); - let renderer = WasmRenderer::new_opt(None, Some(180)).theme(Theme::Walden); + let renderer = WasmRenderer::new_opt(None, Some(178)); on_mount(move || { create_effect(move || { @@ -88,16 +100,100 @@ pub fn LossHistory() -> View { }); view! { - div(id=CONTAINER_ID) + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +// the spectrum of the Hessian during the last realization +#[component] +fn SpectrumHistory() -> View { + const CONTAINER_ID: &str = "spectrum-history"; + let state = use_context::(); + let renderer = WasmRenderer::new(478, 178); + + on_mount(move || { + create_effect(move || { + // get the spectrum of the Hessian at each step + let hess_eigvals = state.assembly.descent_history.with( + |history| history.hess_eigvals + .iter() + .enumerate() + .map( + |(step, eigvals)| eigvals.iter().map( + move |val| vec![step as f64, *val] + ) + ) + .flatten() + .collect::>() + ); + + // initialize the chart axes and series + let step_axis = Axis::new(); + let eigval_axis = Axis::new(); + + // 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 chart = Chart::new() + .animation(false) + .x_axis(step_axis) + .y_axis(eigval_axis) + .grid(Grid::new().top(20).right(40).bottom(30).left(60)) + .series(eigval_series); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +#[component(inline_props)] +fn DiagnosticsPanel(name: &'static str, children: Children) -> View { + let diagnostics_state = use_context::(); + view! { + div( + class="diagnostics-panel", + "hidden"=diagnostics_state.active_tab.with( + |active_tab| { + if active_tab == name { + None + } else { + Some("") + } + } + ) + ) { + (children) + } } } #[component] pub fn Diagnostics() -> View { + let diagnostics_state = DiagnosticsState::new("loss".to_string()); + let active_tab = diagnostics_state.active_tab.clone(); + provide_context(diagnostics_state); + view! { div(id="diagnostics") { - RealizationStatus {} - LossHistory {} + div(id="diagnostics-bar") { + RealizationStatus {} + select(bind:value=active_tab) { + option(value="loss") { "Loss" } + option(value="spectrum") { "Spectrum" } + } + } + DiagnosticsPanel(name="loss") { LossHistory {} } + DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } } } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 6d20df6..b6b1e50 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -256,7 +256,7 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub min_eigval: Vec, + pub hess_eigvals: Vec::>, pub base_step: Vec>, pub backoff_steps: Vec } @@ -267,7 +267,7 @@ impl DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), - min_eigval: Vec::::new(), + hess_eigvals: Vec::>::new(), base_step: Vec::>::new(), backoff_steps: Vec::::new(), } @@ -467,11 +467,12 @@ pub fn realize_gram( hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian - let min_eigval = hess.symmetric_eigenvalues().min(); + let hess_eigvals = hess.symmetric_eigenvalues(); + let min_eigval = hess_eigvals.min(); if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } - history.min_eigval.push(min_eigval); + history.hess_eigvals.push(hess_eigvals); // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace