Rewind through the descent history #114

Open
Vectornaut wants to merge 2 commits from Vectornaut/dyna3:rewind-history into main
6 changed files with 152 additions and 19 deletions
Showing only changes of commit 36036141b5 - Show all commits

View file

@ -184,6 +184,7 @@ details[open]:has(li) .element-switch::after {
#diagnostics-bar { #diagnostics-bar {
display: flex; display: flex;
gap: 8px;
} }
#realization-status { #realization-status {
@ -207,6 +208,14 @@ details[open]:has(li) .element-switch::after {
content: '⚠'; content: '⚠';
} }
#step-input > label {
padding-right: 4px;
}
#step-input > input {
width: 45px;
}
.diagnostics-panel { .diagnostics-panel {
margin-top: 10px; margin-top: 10px;
min-height: 180px; min-height: 180px;

View file

@ -534,6 +534,7 @@ pub struct Assembly {
// realization diagnostics // realization diagnostics
pub realization_status: Signal<Result<(), String>>, pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>, pub descent_history: Signal<DescentHistory>,
pub step: Signal<SpecifiedValue>,
} }
impl Assembly { impl Assembly {
@ -547,20 +548,33 @@ impl Assembly {
realization_trigger: create_signal(()), realization_trigger: create_signal(()),
realization_status: create_signal(Ok(())), realization_status: create_signal(Ok(())),
descent_history: create_signal(DescentHistory::new()), descent_history: create_signal(DescentHistory::new()),
step: create_signal(SpecifiedValue::from_empty_spec()),
}; };
// realize the assembly whenever the element list, the regulator list, // realize the assembly whenever the element list, the regulator list,
// a regulator's set point, or the realization trigger is updated // 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 || { create_effect(move || {
assembly_for_effect.elements.track(); assembly_for_realization.elements.track();
assembly_for_effect.regulators.with( assembly_for_realization.regulators.with(
|regs| for reg in regs { |regs| for reg in regs {
reg.set_point().track(); reg.set_point().track();
} }
); );
assembly_for_effect.realization_trigger.track(); assembly_for_realization.realization_trigger.track();
assembly_for_effect.realize(); 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 assembly
@ -647,6 +661,16 @@ impl Assembly {
}); });
} }
// --- updating the configuration ---
pub fn load_config(&self, config: &DMatrix<f64>) {
for elt in self.elements.get_clone_untracked() {
elt.representation().update(
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
);
}
}
// --- realization --- // --- realization ---
pub fn realize(&self) { pub fn realize(&self) {
@ -696,11 +720,12 @@ impl Assembly {
console_log!("Loss: {}", history.scaled_loss.last().unwrap()); 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); self.descent_history.set(history);
match result { match result {
Ok(ConfigNeighborhood { config, nbhd: tangent }) => { Ok(ConfigNeighborhood { nbhd: tangent, .. }) => {
/* DEBUG */ /* DEBUG */
// report the tangent dimension // report the tangent dimension
console_log!("Tangent dimension: {}", tangent.dim()); console_log!("Tangent dimension: {}", tangent.dim());
@ -708,12 +733,15 @@ impl Assembly {
// report the realization status // report the realization status
self.realization_status.set(Ok(())); self.realization_status.set(Ok(()));
// read out the solution // display the last realization step
for elt in self.elements.get_clone_untracked() { self.step.set(
elt.representation().update( if step_cnt > 0 {
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) let last_step = step_cnt - 1;
); SpecifiedValue::try_from(last_step.to_string()).unwrap()
} } else {
SpecifiedValue::from_empty_spec()
}
);
// save the tangent space // save the tangent space
self.tangent.set_silent(tangent); self.tangent.set_silent(tangent);
@ -723,7 +751,10 @@ impl Assembly {
// setting the status to has a different type than the // setting the status to has a different type than the
// `Err(message)` we received from the match: we're changing the // `Err(message)` we received from the match: we're changing the
// `Ok` type from `Realization` to `()` // `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)));
}, },
} }
} }

View file

@ -7,7 +7,7 @@ use charming::{
}; };
use sycamore::prelude::*; use sycamore::prelude::*;
use crate::AppState; use crate::{AppState, specified::SpecifiedValue};
#[derive(Clone)] #[derive(Clone)]
struct DiagnosticsState { struct DiagnosticsState {
@ -48,6 +48,69 @@ fn RealizationStatus() -> View {
} }
} }
// history step input
#[component]
fn StepInput() -> View {
// get the assembly
let state = use_context::<AppState>();
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<Option<f64>> { fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
vec![ vec![
Some(step as f64), Some(step as f64),
@ -248,6 +311,7 @@ pub fn Diagnostics() -> View {
option(value = "loss") { "Loss" } option(value = "loss") { "Loss" }
option(value = "spectrum") { "Spectrum" } option(value = "spectrum") { "Spectrum" }
} }
StepInput {}
} }
DiagnosticsPanel(name = "loss") { LossHistory {} } DiagnosticsPanel(name = "loss") { LossHistory {} }
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }

View file

@ -588,7 +588,25 @@ pub fn Display() -> View {
location_z *= (time_step * ZOOM_SPEED * zoom).exp(); location_z *= (time_step * ZOOM_SPEED * zoom).exp();
// manipulate the assembly // 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( let sel = state.selection.with(
|sel| sel.into_iter().next().unwrap().clone() |sel| sel.into_iter().next().unwrap().clone()
); );

View file

@ -353,7 +353,7 @@ fn seek_better_config(
// a first-order neighborhood of a configuration // a first-order neighborhood of a configuration
pub struct ConfigNeighborhood { pub struct ConfigNeighborhood {
pub config: DMatrix<f64>, #[cfg(feature = "dev")] pub config: DMatrix<f64>,
pub nbhd: ConfigSubspace, pub nbhd: ConfigSubspace,
} }
@ -388,7 +388,7 @@ pub fn realize_gram(
if assembly_dim == 0 { if assembly_dim == 0 {
let result = Ok( let result = Ok(
ConfigNeighborhood { ConfigNeighborhood {
config: guess.clone(), #[cfg(feature = "dev")] config: guess.clone(),
nbhd: ConfigSubspace::zero(0), nbhd: ConfigSubspace::zero(0),
} }
); );
@ -509,7 +509,7 @@ pub fn realize_gram(
// find the kernel of the Hessian. give it the uniform inner product // find the kernel of the Hessian. give it the uniform inner product
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); 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 { } else {
Err("Failed to reach target accuracy".to_string()) Err("Failed to reach target accuracy".to_string())
}; };

View file

@ -26,6 +26,17 @@ impl SpecifiedValue {
} }
} }
// a `SpecifiedValue` can be constructed from a floating-point option, which is
// given a canonical specification
impl From<Option<f64>> for SpecifiedValue {
fn from(value: Option<f64>) -> 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 // a `SpecifiedValue` can be constructed from a specification string, formatted
// as described in the comment on the structure definition. the result is `Ok` // as described in the comment on the structure definition. the result is `Ok`
// if the specification is properly formatted, and `Error` if not // if the specification is properly formatted, and `Error` if not