From 36036141b5fca0950d4a3c9399a02a19cd2a46c0 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 21 Aug 2025 14:49:32 -0400 Subject: [PATCH] Rewind through the descent history --- app-proto/main.css | 9 ++++ app-proto/src/assembly.rs | 59 ++++++++++++++++------ app-proto/src/components/diagnostics.rs | 66 ++++++++++++++++++++++++- app-proto/src/components/display.rs | 20 +++++++- app-proto/src/engine.rs | 6 +-- app-proto/src/specified.rs | 11 +++++ 6 files changed, 152 insertions(+), 19 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 7981285..a00d309 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -184,6 +184,7 @@ details[open]:has(li) .element-switch::after { #diagnostics-bar { display: flex; + gap: 8px; } #realization-status { @@ -207,6 +208,14 @@ details[open]:has(li) .element-switch::after { content: '⚠'; } +#step-input > label { + padding-right: 4px; +} + +#step-input > input { + width: 45px; +} + .diagnostics-panel { margin-top: 10px; min-height: 180px; diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 94e7b3c..669c0d0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -534,6 +534,7 @@ pub struct Assembly { // realization diagnostics pub realization_status: Signal>, pub descent_history: Signal, + pub step: Signal, } impl Assembly { @@ -547,20 +548,33 @@ impl Assembly { realization_trigger: create_signal(()), realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()), + step: create_signal(SpecifiedValue::from_empty_spec()), }; // realize the assembly whenever the element list, the regulator list, // a regulator's set point, or the realization trigger is updated - let assembly_for_effect = assembly.clone(); + let assembly_for_realization = assembly.clone(); create_effect(move || { - assembly_for_effect.elements.track(); - assembly_for_effect.regulators.with( + assembly_for_realization.elements.track(); + assembly_for_realization.regulators.with( |regs| for reg in regs { reg.set_point().track(); } ); - assembly_for_effect.realization_trigger.track(); - assembly_for_effect.realize(); + assembly_for_realization.realization_trigger.track(); + assembly_for_realization.realize(); + }); + + // load a configuration from the descent history whenever the active + // step is updated + let assembly_for_step_selection = assembly.clone(); + create_effect(move || { + if let Some(step) = assembly.step.with(|n| n.value) { + let config = assembly.descent_history.with_untracked( + |history| history.config[step as usize].clone() + ); + assembly_for_step_selection.load_config(&config) + } }); assembly @@ -647,6 +661,16 @@ impl Assembly { }); } + // --- updating the configuration --- + + pub fn load_config(&self, config: &DMatrix) { + for elt in self.elements.get_clone_untracked() { + elt.representation().update( + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) + ); + } + } + // --- realization --- pub fn realize(&self) { @@ -696,11 +720,12 @@ impl Assembly { console_log!("Loss: {}", history.scaled_loss.last().unwrap()); } - // report the loss history + // report the descent history + let step_cnt = history.config.len(); self.descent_history.set(history); match result { - Ok(ConfigNeighborhood { config, nbhd: tangent }) => { + Ok(ConfigNeighborhood { nbhd: tangent, .. }) => { /* DEBUG */ // report the tangent dimension console_log!("Tangent dimension: {}", tangent.dim()); @@ -708,12 +733,15 @@ impl Assembly { // report the realization status self.realization_status.set(Ok(())); - // read out the solution - for elt in self.elements.get_clone_untracked() { - elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) - ); - } + // display the last realization step + self.step.set( + if step_cnt > 0 { + let last_step = step_cnt - 1; + SpecifiedValue::try_from(last_step.to_string()).unwrap() + } else { + SpecifiedValue::from_empty_spec() + } + ); // save the tangent space self.tangent.set_silent(tangent); @@ -723,7 +751,10 @@ impl Assembly { // setting the status to has a different type than the // `Err(message)` we received from the match: we're changing the // `Ok` type from `Realization` to `()` - self.realization_status.set(Err(message)) + self.realization_status.set(Err(message)); + + // display the initial guess + self.step.set(SpecifiedValue::from(Some(0.0))); }, } } diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index e265982..51d58f1 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -7,7 +7,7 @@ use charming::{ }; use sycamore::prelude::*; -use crate::AppState; +use crate::{AppState, specified::SpecifiedValue}; #[derive(Clone)] struct DiagnosticsState { @@ -48,6 +48,69 @@ fn RealizationStatus() -> View { } } +// history step input +#[component] +fn StepInput() -> View { + // get the assembly + let state = use_context::(); + let assembly = state.assembly; + + // the `last_step` signal holds the index of the last step + let last_step = assembly.descent_history.map( + |history| match history.config.len() { + 0 => None, + n => Some(n - 1), + } + ); + let input_max = last_step.map(|last| last.unwrap_or(0)); + + // these signals hold the entered step number + let value = create_signal(String::new()); + let value_as_number = create_signal(0.0); + + create_effect(move || { + value.set(assembly.step.with(|n| n.spec.clone())); + }); + + view! { + div(id = "step-input") { + label { "Step" } + input( + r#type = "number", + min = "0", + max = input_max.with(|max| max.to_string()), + bind:value = value, + bind:valueAsNumber = value_as_number, + on:change = move |_| { + if last_step.with(|last| last.is_some()) { + // clamp the step within its allowed range. the lower + // bound is redundant on browsers that make it + // impossible to type negative values into a number + // input with a non-negative `min`, but there's no harm + // in being careful + let step_raw = value.with( + |val| SpecifiedValue::try_from(val.clone()) + .unwrap_or(SpecifiedValue::from_empty_spec() + ) + ); + let step = SpecifiedValue::from( + step_raw.value.map( + |val| val.clamp(0.0, input_max.get() as f64) + ) + ); + + // set the input string and the assembly's active step + value.set(step.spec.clone()); + assembly.step.set(step); + } else { + value.set(String::new()); + } + }, + ) + } + } +} + fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { vec![ Some(step as f64), @@ -248,6 +311,7 @@ pub fn Diagnostics() -> View { option(value = "loss") { "Loss" } option(value = "spectrum") { "Spectrum" } } + StepInput {} } DiagnosticsPanel(name = "loss") { LossHistory {} } DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index da921dd..c4803dc 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -588,7 +588,25 @@ pub fn Display() -> View { location_z *= (time_step * ZOOM_SPEED * zoom).exp(); // manipulate the assembly - if state.selection.with(|sel| sel.len() == 1) { + /* KLUDGE */ + // to avoid the complexity of making tangent space projection + // conditional and dealing with unnormalized representation vectors, + // we only allow manipulation when we're looking at the last step of + // a successful realization + let realization_successful = state.assembly.realization_status.with( + |status| status.is_ok() + ); + let step_val = state.assembly.step.with_untracked(|step| step.value); + let on_last_step = step_val.is_some_and( + |n| state.assembly.descent_history.with_untracked( + |history| n as usize + 1 == history.config.len().max(1) + ) + ); + if + state.selection.with(|sel| sel.len() == 1) + && realization_successful + && on_last_step + { let sel = state.selection.with( |sel| sel.into_iter().next().unwrap().clone() ); diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index d033c01..ef150a0 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -353,7 +353,7 @@ fn seek_better_config( // a first-order neighborhood of a configuration pub struct ConfigNeighborhood { - pub config: DMatrix, + #[cfg(feature = "dev")] pub config: DMatrix, pub nbhd: ConfigSubspace, } @@ -388,7 +388,7 @@ pub fn realize_gram( if assembly_dim == 0 { let result = Ok( ConfigNeighborhood { - config: guess.clone(), + #[cfg(feature = "dev")] config: guess.clone(), nbhd: ConfigSubspace::zero(0), } ); @@ -509,7 +509,7 @@ pub fn realize_gram( // find the kernel of the Hessian. give it the uniform inner product let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); - Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) + Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent }) } else { Err("Failed to reach target accuracy".to_string()) }; diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index 788460b..b0f04b5 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -26,6 +26,17 @@ impl SpecifiedValue { } } +// a `SpecifiedValue` can be constructed from a floating-point option, which is +// given a canonical specification +impl From> for SpecifiedValue { + fn from(value: Option) -> Self { + match value { + Some(x) => SpecifiedValue{ spec: x.to_string(), value }, + None => SpecifiedValue::from_empty_spec(), + } + } +} + // a `SpecifiedValue` can be constructed from a specification string, formatted // as described in the comment on the structure definition. the result is `Ok` // if the specification is properly formatted, and `Error` if not