Add a spectrum history panel
All checks were successful
/ test (pull_request) Successful in 3m36s

This introduces a framework for adding more diagnostics panels.
This commit is contained in:
Aaron Fenyes 2025-06-11 01:21:18 -07:00
parent 059224e269
commit 0be7448e24
3 changed files with 124 additions and 14 deletions

View file

@ -182,14 +182,23 @@ details[open]:has(li) .element-switch::after {
margin: 10px; margin: 10px;
} }
#diagnostics-bar {
display: flex;
}
#realization-status { #realization-status {
display: flex; display: flex;
flex-grow: 1;
} }
#realization-status .status { #realization-status .status {
margin-right: 4px; margin-right: 4px;
} }
#realization-status :not(.status) {
flex-grow: 1;
}
#realization-status .status::after { #realization-status .status::after {
content: '✓'; content: '✓';
} }
@ -198,8 +207,12 @@ details[open]:has(li) .element-switch::after {
content: '⚠'; content: '⚠';
} }
#loss-history { .diagnostics-panel {
margin-top: 10px; margin-top: 10px;
min-height: 180px;
}
.diagnostics-chart {
background-color: var(--display-background); background-color: var(--display-background);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;

View file

@ -3,16 +3,28 @@ use charming::{
WasmRenderer, WasmRenderer,
component::{Axis, Grid}, component::{Axis, Grid},
element::AxisType, element::AxisType,
series::Line, series::{Line, Scatter},
theme::Theme
}; };
use sycamore::prelude::*; use sycamore::prelude::*;
use crate::AppState; use crate::AppState;
#[derive(Clone)]
struct DiagnosticsState {
active_tab: Signal<String>
}
impl DiagnosticsState {
fn new(initial_tab: String) -> DiagnosticsState {
DiagnosticsState {
active_tab: create_signal(initial_tab)
}
}
}
// a realization status indicator // a realization status indicator
#[component] #[component]
pub fn RealizationStatus() -> View { fn RealizationStatus() -> View {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let realization_status = state.assembly.realization_status; let realization_status = state.assembly.realization_status;
view! { 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] #[component]
pub fn LossHistory() -> View { fn LossHistory() -> View {
const CONTAINER_ID: &str = "loss-history"; const CONTAINER_ID: &str = "loss-history";
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let renderer = WasmRenderer::new_opt(None, Some(180)).theme(Theme::Walden); let renderer = WasmRenderer::new_opt(None, Some(178));
on_mount(move || { on_mount(move || {
create_effect(move || { create_effect(move || {
@ -88,16 +100,100 @@ pub fn LossHistory() -> View {
}); });
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::<AppState>();
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::<Vec<_>>()
);
// 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::<f64>, None::<f64>]);
}
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::<DiagnosticsState>();
view! {
div(
class="diagnostics-panel",
"hidden"=diagnostics_state.active_tab.with(
|active_tab| {
if active_tab == name {
None
} else {
Some("")
}
}
)
) {
(children)
}
} }
} }
#[component] #[component]
pub fn Diagnostics() -> View { 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! { view! {
div(id="diagnostics") { div(id="diagnostics") {
RealizationStatus {} div(id="diagnostics-bar") {
LossHistory {} RealizationStatus {}
select(bind:value=active_tab) {
option(value="loss") { "Loss" }
option(value="spectrum") { "Spectrum" }
}
}
DiagnosticsPanel(name="loss") { LossHistory {} }
DiagnosticsPanel(name="spectrum") { SpectrumHistory {} }
} }
} }
} }

View file

@ -256,7 +256,7 @@ pub struct DescentHistory {
pub config: Vec<DMatrix<f64>>, pub config: Vec<DMatrix<f64>>,
pub scaled_loss: Vec<f64>, pub scaled_loss: Vec<f64>,
pub neg_grad: Vec<DMatrix<f64>>, pub neg_grad: Vec<DMatrix<f64>>,
pub min_eigval: Vec<f64>, pub hess_eigvals: Vec::<DVector<f64>>,
pub base_step: Vec<DMatrix<f64>>, pub base_step: Vec<DMatrix<f64>>,
pub backoff_steps: Vec<i32> pub backoff_steps: Vec<i32>
} }
@ -267,7 +267,7 @@ impl DescentHistory {
config: Vec::<DMatrix<f64>>::new(), config: Vec::<DMatrix<f64>>::new(),
scaled_loss: Vec::<f64>::new(), scaled_loss: Vec::<f64>::new(),
neg_grad: Vec::<DMatrix<f64>>::new(), neg_grad: Vec::<DMatrix<f64>>::new(),
min_eigval: Vec::<f64>::new(), hess_eigvals: Vec::<DVector<f64>>::new(),
base_step: Vec::<DMatrix<f64>>::new(), base_step: Vec::<DMatrix<f64>>::new(),
backoff_steps: Vec::<i32>::new(), backoff_steps: Vec::<i32>::new(),
} }
@ -467,11 +467,12 @@ pub fn realize_gram(
hess = DMatrix::from_columns(hess_cols.as_slice()); hess = DMatrix::from_columns(hess_cols.as_slice());
// regularize the Hessian // 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 { if min_eigval <= 0.0 {
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); 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 // project the negative gradient and negative Hessian onto the
// orthogonal complement of the frozen subspace // orthogonal complement of the frozen subspace