From a4d081f684cb3b917c49736f85c6564d0fd96c31 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:18:37 -0700 Subject: [PATCH 01/24] Update Sycamore to 0.9.1 --- app-proto/Cargo.lock | 25 +++++++++++++------------ app-proto/Cargo.toml | 2 +- app-proto/src/outline.rs | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 3bf609c..55e8686 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -423,9 +423,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "sycamore" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ "hashbrown", "indexmap", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "sycamore-core" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ "hashbrown", "paste", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "sycamore-macro" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137" dependencies = [ "once_cell", "proc-macro2", @@ -465,20 +465,21 @@ dependencies = [ [[package]] name = "sycamore-reactive" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add" dependencies = [ "paste", "slotmap", "smallvec", + "wasm-bindgen", ] [[package]] name = "sycamore-view-parser" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033" dependencies = [ "proc-macro2", "quote", @@ -487,9 +488,9 @@ dependencies = [ [[package]] name = "sycamore-web" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd" dependencies = [ "html-escape", "js-sys", diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 9b46b2b..6932b72 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -15,7 +15,7 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -sycamore = "0.9.0-beta.3" +sycamore = "0.9.1" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 59bbdcc..77d8575 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -151,7 +151,7 @@ fn ElementOutlineItem(element: Rc) -> View { .clone() .into_iter() .sorted_by_key(|reg| reg.subjects().len()) - .collect() + .collect::>() ); let details_node = create_node_ref(); view! { @@ -241,7 +241,7 @@ pub fn Outline() -> View { .clone() .into_iter() .sorted_by_key(|elt| elt.id().clone()) - .collect() + .collect::>() ); view! { From d4302d237b1b7ce1cea72634b0e0e8605f116ebb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 02/24] Encapsulate realization results In the process, spruce up our realization diagnostics logging and factor out some of the repetitive code in the examples, because we're already changing those parts of the code to adapt them to the new encapsulation. This commit changes the example output format. I've checked by hand that the output is rearranged but not meaningfully changed. --- app-proto/examples/common/mod.rs | 36 +++++++++++++++ app-proto/examples/irisawa-hexlet.rs | 33 +++++++------- app-proto/examples/kaleidocycle.rs | 53 ++++++++++++---------- app-proto/examples/point-on-sphere.rs | 38 +++++++++------- app-proto/examples/three-spheres.rs | 29 ++++++------ app-proto/run-examples | 6 +-- app-proto/src/assembly.rs | 21 +++++---- app-proto/src/engine.rs | 63 ++++++++++++++++----------- 8 files changed, 176 insertions(+), 103 deletions(-) create mode 100644 app-proto/examples/common/mod.rs diff --git a/app-proto/examples/common/mod.rs b/app-proto/examples/common/mod.rs new file mode 100644 index 0000000..b1b91b5 --- /dev/null +++ b/app-proto/examples/common/mod.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use nalgebra::DMatrix; + +use dyna3::engine::{Q, DescentHistory, RealizationResult}; + +pub fn print_title(title: &str) { + println!("─── {title} ───"); +} + +pub fn print_realization_diagnostics(realization_result: &RealizationResult) { + let RealizationResult { result, history } = realization_result; + println!(); + if let Err(ref msg) = result { + println!("❌️ {msg}"); + } else { + println!("✅️ Target accuracy achieved!"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); +} + +pub fn print_gram_matrix(config: &DMatrix) { + println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); +} + +pub fn print_config(config: &DMatrix) { + println!("\nConfiguration:{}", config.to_string().trim_end()); +} + +pub fn print_loss_history(history: &DescentHistory) { + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 639a494..750a0d0 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,25 +1,28 @@ -use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; +mod common; + +use common::{ + print_gram_matrix, + print_loss_history, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{Realization, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + let realization_result = realize_irisawa_hexlet(SCALED_TOL); + print_title("Irisawa hexlet"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization { config, .. }) = realization_result.result { + // print the diameters of the chain spheres println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } + + // print the completed Gram matrix + print_gram_matrix(&config); } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } + print_loss_history(&realization_result.history); } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 2779ab1..21f8187 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,30 +1,37 @@ +mod common; + use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{Q, examples::realize_kaleidocycle}; +use common::{ + print_config, + print_gram_matrix, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{Realization, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); + let realization_result = realize_kaleidocycle(SCALED_TOL); + print_title("Kaleidocycle"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization { config, tangent }) = realization_result.result { + // print the completed Gram matrix and the realized configuration + print_gram_matrix(&config); + print_config(&config); + + // find the kaleidocycle's twist motion by projecting onto the tangent + // space + const N_POINTS: usize = 12; + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - - // find the kaleidocycle's twist motion by projecting onto the tangent space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1) - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - print!("Twist motion:{}", normalization * twist_motion); } \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 880d7b0..774368e 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,4 +1,19 @@ -use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; +mod common; + +use common::{ + print_config, + print_gram_matrix, + print_loss_history, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{ + point, + realize_gram, + sphere, + ConstraintProblem, + Realization +}; fn main() { let mut problem = ConstraintProblem::from_guess(&[ @@ -11,21 +26,14 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - println!(); - let (config, _, success, history) = realize_gram( + let realization_result = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print_title("Point on a sphere"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization{ config, .. }) = realization_result.result { + print_gram_matrix(&config); + print_config(&config); } + print_loss_history(&realization_result.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 3f3cc44..dd2cdc0 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,4 +1,12 @@ -use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; +mod common; + +use common::{ + print_gram_matrix, + print_loss_history, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{realize_gram, sphere, ConstraintProblem, Realization}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -14,20 +22,13 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - println!(); - let (config, _, success, history) = realize_gram( + let realization_result = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print_title("Three spheres"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization{ config, .. }) = realization_result.result { + print_gram_matrix(&config); } + print_loss_history(&realization_result.history); } \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index 52173b0..b3e3121 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -6,7 +6,7 @@ # http://xion.io/post/code/rust-examples.html # -cargo run --example irisawa-hexlet -cargo run --example three-spheres -cargo run --example point-on-sphere +cargo run --example irisawa-hexlet; echo +cargo run --example three-spheres; echo +cargo run --example point-on-sphere; echo cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 6c91fc0..9f66055 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -24,7 +24,9 @@ use crate::{ realize_gram, sphere, ConfigSubspace, - ConstraintProblem + ConstraintProblem, + Realization, + RealizationResult }, outline::OutlineItem, specified::SpecifiedValue @@ -687,22 +689,25 @@ impl Assembly { console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix - let (config, tangent, success, history) = realize_gram( + let RealizationResult { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ // report the outcome of the search - if success { - console_log!("Target accuracy achieved!") + if let Err(ref msg) = result { + console_log!("❌️ {msg}"); } else { - console_log!("Failed to reach target accuracy") + console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); - console_log!("Tangent dimension: {}", tangent.dim()); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + if let Ok(Realization { config, tangent }) = result { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + // read out the solution for elt in self.elements.get_clone_untracked() { elt.representation().update( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index c5d7b00..2524fcd 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -393,6 +393,16 @@ fn seek_better_config( None } +pub struct Realization { + pub config: DMatrix, + pub tangent: ConfigSubspace +} + +pub struct RealizationResult { + pub result: Result, + pub history: DescentHistory +} + // seek a matrix `config` that matches the partial matrix `problem.frozen` and // has `config' * Q * config` matching the partial matrix `problem.gram`. start // at `problem.guess`, set the frozen entries to their desired values, and then @@ -405,7 +415,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { +) -> RealizationResult { // destructure the problem data let ConstraintProblem { gram, guess, frozen @@ -491,19 +501,20 @@ pub fn realize_gram( history.base_step.push(base_step.clone()); // use backtracking line search to find a better configuration - match seek_better_config( + if let Some((better_state, backoff_steps)) = seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), min_efficiency, backoff, max_backoff_steps ) { - Some((better_state, backoff_steps)) => { - state = better_state; - history.backoff_steps.push(backoff_steps); - }, - None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) + state = better_state; + history.backoff_steps.push(backoff_steps); + } else { + return RealizationResult { + result: Err("Line search failed".to_string()), + history + } }; } - let success = state.loss < tol; - let tangent = if success { + let result = if state.loss < tol { // express the uniform basis in the standard basis const UNIFORM_DIM: usize = 4; let total_dim_unif = UNIFORM_DIM * assembly_dim; @@ -516,11 +527,13 @@ pub fn realize_gram( } // find the kernel of the Hessian. give it the uniform inner product - ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) + let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); + + Ok(Realization { config: state.config, tangent }) } else { - ConfigSubspace::zero(assembly_dim) + Err("Failed to reach target accuracy".to_string()) }; - (state.config, tangent, success, history) + RealizationResult{ result, history } } // --- tests --- @@ -539,7 +552,7 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> RealizationResult { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -590,7 +603,7 @@ pub mod examples { // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space - pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_kaleidocycle(scaled_tol: f64) -> RealizationResult { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -714,10 +727,10 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let (config, _, success, history) = realize_gram( + let RealizationResult { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(success, true); + let config = result.unwrap().config; for base_step in history.base_step.into_iter() { for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); @@ -732,7 +745,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -759,11 +772,11 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let (config, tangent, success, history) = realize_gram( + let RealizationResult { result, history } = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let Realization { config, tangent } = result.unwrap(); assert_eq!(config, problem.guess); - assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -831,8 +844,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - assert_eq!(success, true); + let RealizationResult { result, history } = realize_kaleidocycle(SCALED_TOL); + let Realization { config, tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -920,11 +933,11 @@ mod tests { problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + let RealizationResult { result: result_orig, history: history_orig } = realize_gram( &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let Realization { config: config_orig, tangent: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); - assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); // find another pair of spheres that meet at 120°. we'll think of this @@ -941,11 +954,11 @@ mod tests { guess: guess_tfm, frozen: problem_orig.frozen }; - let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + let RealizationResult { result: result_tfm, history: history_tfm } = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let Realization { config: config_tfm, tangent: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); - assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); // project a nudge to the tangent space of the solution variety at the From 6d2e3d776b2a0aa81cdbd0f60c4da5dfd6eeb330 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 03/24] Show the loss history from the last realization This introduces a dependency on the Charming crate, which we use to plot the loss history, and the ECharts JavaScript library, which Charming depends on. Now that there's more than one canvas on the page, we have to pick out the display by ID rather than by element type in our style sheet. --- app-proto/Cargo.lock | 565 ++++++++++++++++++++++++++++++++++- app-proto/Cargo.toml | 3 + app-proto/index.html | 6 + app-proto/main.css | 10 +- app-proto/src/add_remove.rs | 4 +- app-proto/src/assembly.rs | 12 +- app-proto/src/diagnostics.rs | 65 ++++ app-proto/src/display.rs | 1 + app-proto/src/engine.rs | 2 +- app-proto/src/main.rs | 3 + 10 files changed, 660 insertions(+), 11 deletions(-) create mode 100644 app-proto/src/diagnostics.rs diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 55e8686..4f75c45 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -20,6 +20,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "approx" version = "0.5.1" @@ -35,6 +50,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -68,6 +98,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charming" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09" +dependencies = [ + "handlebars", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_with", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -78,10 +137,122 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyna3" version = "0.1.0" dependencies = [ + "charming", "console_error_panic_hook", "dyna3", "itertools", @@ -106,6 +277,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -117,6 +304,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -127,6 +336,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html-escape" version = "0.2.13" @@ -136,6 +351,47 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -143,7 +399,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -155,6 +412,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.70" @@ -192,6 +455,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minicov" version = "0.3.5" @@ -248,6 +517,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -257,6 +532,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -289,6 +579,57 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -363,6 +704,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.2" @@ -387,6 +734,90 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -421,14 +852,20 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "sycamore" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ - "hashbrown", - "indexmap", + "hashbrown 0.14.5", + "indexmap 2.5.0", "paste", "sycamore-core", "sycamore-macro", @@ -444,7 +881,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "paste", "sycamore-reactive", ] @@ -506,21 +943,78 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -677,6 +1171,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 6932b72..1230b47 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -17,6 +17,9 @@ nalgebra = "0.33.0" readonly = "0.2.12" sycamore = "0.9.1" +# We use Charming to help display engine diagnostics +charming = { version = "0.5.1", features = ["wasm"] } + # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for diff --git a/app-proto/index.html b/app-proto/index.html index 92238f4..4fbe52f 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,6 +6,12 @@ + + + diff --git a/app-proto/main.css b/app-proto/main.css index d56784f..fb16698 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -172,9 +172,17 @@ details[open]:has(li) .element-switch::after { color: var(--text-invalid); } +/* diagnostics */ + +#loss-history { + margin: 10px; + background-color: var(--display-background); + border-radius: 8px; +} + /* display */ -canvas { +#display { float: left; margin-left: 20px; margin-top: 20px; diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index f3bbc97..d737c79 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -3,8 +3,9 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ - engine, AppState, + engine, + engine::DescentHistory, assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; @@ -195,6 +196,7 @@ pub fn AddRemove() -> View { assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); state.selection.update(|sel| sel.clear()); // load assembly diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 9f66055..f466108 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -25,6 +25,7 @@ use crate::{ sphere, ConfigSubspace, ConstraintProblem, + DescentHistory, Realization, RealizationResult }, @@ -549,7 +550,10 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal>> + pub elements_by_id: Signal>>, + + // realization diagnostics + pub descent_history: Signal } impl Assembly { @@ -558,7 +562,8 @@ impl Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()) + elements_by_id: create_signal(BTreeMap::default()), + descent_history: create_signal(DescentHistory::new()) } } @@ -703,6 +708,9 @@ impl Assembly { console_log!("Steps: {}", history.scaled_loss.len() - 1); console_log!("Loss: {}", history.scaled_loss.last().unwrap()); + // record realization diagnostics + self.descent_history.set(history); + if let Ok(Realization { config, tangent }) = result { /* DEBUG */ // report the tangent dimension diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs new file mode 100644 index 0000000..6d3d145 --- /dev/null +++ b/app-proto/src/diagnostics.rs @@ -0,0 +1,65 @@ +use charming::{ + Chart, + WasmRenderer, + component::{Axis, Grid}, + element::AxisType, + series::Line, + theme::Theme +}; +use sycamore::prelude::*; + +use crate::AppState; + +// a plot of the loss history from the last realization +#[component] +pub fn Diagnostics() -> View { + const CONTAINER_ID: &str = "loss-history"; + let state = use_context::(); + let renderer = WasmRenderer::new_opt(None, Some(180)).theme(Theme::Walden); + + on_mount(move || { + create_effect(move || { + // get the loss history + let scaled_loss = state.assembly.descent_history.with( + |history| history.scaled_loss.clone() + ); + let step_cnt = scaled_loss.len(); + + // initialize the chart axes and series + const MIN_INTERVAL: f64 = 0.01; + let mut 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); + + // 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::]); + } + let chart = Chart::new() + .animation(false) + .x_axis(step_axis) + .y_axis(scaled_loss_axis) + .grid(Grid::new().top(20).right(40).bottom(30).left(60)) + .series(scaled_loss_series); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID) + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 69a3659..1646c4e 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -806,6 +806,7 @@ pub fn Display() -> View { // again canvas( ref=display, + id="display", width="600", height="600", tabindex="0", diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 2524fcd..38003c9 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -262,7 +262,7 @@ pub struct DescentHistory { } impl DescentHistory { - fn new() -> DescentHistory { + pub fn new() -> DescentHistory { DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index b76859a..f905c46 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,5 +1,6 @@ mod add_remove; mod assembly; +mod diagnostics; mod display; mod engine; mod outline; @@ -13,6 +14,7 @@ use sycamore::prelude::*; use add_remove::AddRemove; use assembly::{Assembly, Element}; +use diagnostics::Diagnostics; use display::Display; use outline::Outline; @@ -60,6 +62,7 @@ fn main() { div(id="sidebar") { AddRemove {} Outline {} + Diagnostics {} } Display {} } From b54a8a92e74878b43cfd5d417e36d6f177e334f0 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 04/24] Add a realization status indicator --- app-proto/examples/common/mod.rs | 4 +-- app-proto/main.css | 42 ++++++++++++++++++++++------ app-proto/src/assembly.rs | 48 +++++++++++++++++++++----------- app-proto/src/diagnostics.rs | 40 +++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 29 deletions(-) diff --git a/app-proto/examples/common/mod.rs b/app-proto/examples/common/mod.rs index b1b91b5..9b6c492 100644 --- a/app-proto/examples/common/mod.rs +++ b/app-proto/examples/common/mod.rs @@ -11,8 +11,8 @@ pub fn print_title(title: &str) { pub fn print_realization_diagnostics(realization_result: &RealizationResult) { let RealizationResult { result, history } = realization_result; println!(); - if let Err(ref msg) = result { - println!("❌️ {msg}"); + if let Err(ref message) = result { + println!("❌️ {message}"); } else { println!("✅️ Target accuracy achieved!"); } diff --git a/app-proto/main.css b/app-proto/main.css index fb16698..a73d5a5 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -18,6 +18,17 @@ body { font-family: 'Fira Sans', sans-serif; } +.invalid { + color: var(--text-invalid); +} + +.status { + width: 20px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + /* sidebar */ #sidebar { @@ -138,6 +149,7 @@ details[open]:has(li) .element-switch::after { } .regulator-input { + margin-right: 4px; color: inherit; background-color: inherit; border: 1px solid var(--border); @@ -159,14 +171,6 @@ details[open]:has(li) .element-switch::after { border-color: var(--border-invalid); } -.status { - width: 20px; - padding-left: 4px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); @@ -174,8 +178,28 @@ details[open]:has(li) .element-switch::after { /* diagnostics */ -#loss-history { +#diagnostics { margin: 10px; +} + +#realization-status { + display: flex; +} + +#realization-status .status { + margin-right: 4px; +} + +#realization-status .status::after { + content: '✓'; +} + +#realization-status.invalid .status::after { + content: '⚠'; +} + +#loss-history { + margin-top: 10px; background-color: var(--display-background); border-radius: 8px; } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index f466108..9a4b934 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -553,6 +553,7 @@ pub struct Assembly { pub elements_by_id: Signal>>, // realization diagnostics + pub realization_status: Signal>, pub descent_history: Signal } @@ -563,6 +564,7 @@ impl Assembly { regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(BTreeMap::default()), + realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()) } } @@ -699,32 +701,44 @@ impl Assembly { ); /* DEBUG */ - // report the outcome of the search - if let Err(ref msg) = result { - console_log!("❌️ {msg}"); + // report the outcome of the search in the browser console + if let Err(ref message) = result { + console_log!("❌️ {message}"); } else { console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - // record realization diagnostics + // report the loss history self.descent_history.set(history); - if let Ok(Realization { config, tangent }) = result { - /* DEBUG */ - // report the tangent dimension - console_log!("Tangent dimension: {}", tangent.dim()); - - // 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())) - ); + match result { + Ok(Realization { config, tangent }) => { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + + // 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())) + ); + } + + // save the tangent space + self.tangent.set_silent(tangent); + }, + Err(message) => { + // report the realization status. the `Err(message)` we're + // 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)) } - - // save the tangent space - self.tangent.set_silent(tangent); } } diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index 6d3d145..a73a793 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -10,9 +10,37 @@ use sycamore::prelude::*; use crate::AppState; +// a realization status indicator +#[component] +pub fn RealizationStatus() -> View { + let state = use_context::(); + let realization_status = state.assembly.realization_status; + view! { + div( + id="realization-status", + class=realization_status.with( + |status| match status { + Ok(_) => "", + Err(_) => "invalid" + } + ) + ) { + div(class="status") + div { + (realization_status.with( + |status| match status { + Ok(_) => "Target accuracy achieved".to_string(), + Err(message) => message.clone() + } + )) + } + } + } +} + // a plot of the loss history from the last realization #[component] -pub fn Diagnostics() -> View { +pub fn LossHistory() -> View { const CONTAINER_ID: &str = "loss-history"; let state = use_context::(); let renderer = WasmRenderer::new_opt(None, Some(180)).theme(Theme::Walden); @@ -62,4 +90,14 @@ pub fn Diagnostics() -> View { view! { div(id=CONTAINER_ID) } +} + +#[component] +pub fn Diagnostics() -> View { + view! { + div(id="diagnostics") { + RealizationStatus {} + LossHistory {} + } + } } \ No newline at end of file From 992b41108f03ab1dfe6967f403e5e477e212e83e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 05/24] Let the Cholesky decomposition fail gracefully --- app-proto/src/engine.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 38003c9..6d20df6 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -490,13 +490,22 @@ pub fn realize_gram( if state.loss < tol { break; } // compute the Newton step + /* TO DO */ /* - we need to either handle or eliminate the case where the minimum - eigenvalue of the Hessian is zero, so the regularized Hessian is - singular. right now, this causes the Cholesky decomposition to return - `None`, leading to a panic when we unrap + we should change our regularization to ensure that the Hessian is + is positive-definite, rather than just positive-semidefinite. ideally, + that would guarantee the success of the Cholesky decomposition--- + although we'd still need the error-handling routine in case of + numerical hiccups */ - let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); + let hess_cholesky = match hess.clone().cholesky() { + Some(cholesky) => cholesky, + None => return RealizationResult { + result: Err("Cholesky decomposition failed".to_string()), + history + } + }; + let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); From e4725174e5e2b2de1d23e1a2a403814c9cb37173 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 06/24] Fix the display's focus indicator --- app-proto/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/main.css b/app-proto/main.css index a73d5a5..8b96942 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -215,7 +215,7 @@ details[open]:has(li) .element-switch::after { border-radius: 16px; } -canvas:focus { +#display:focus { border-color: var(--border-focus-dark); outline: none; } From 059224e2690004b7a23680fe67da8784b16b763a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 07/24] Put a border around the loss history chart --- app-proto/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app-proto/main.css b/app-proto/main.css index 8b96942..14276dd 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -201,6 +201,7 @@ details[open]:has(li) .element-switch::after { #loss-history { margin-top: 10px; background-color: var(--display-background); + border: 1px solid var(--border); border-radius: 8px; } From 0be7448e2475b990572ed3c7989eaa52eabf980b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 11 Jun 2025 01:21:18 -0700 Subject: [PATCH 08/24] 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 From 99136498c7c46dd1578ecc8ee36f221a394d293e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 17 Jun 2025 14:07:51 -0700 Subject: [PATCH 09/24] Add data zoom controls to the diagnostics charts --- app-proto/src/diagnostics.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index b90989f..d283c51 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -1,7 +1,7 @@ use charming::{ Chart, WasmRenderer, - component::{Axis, Grid}, + component::{Axis, DataZoom, Grid}, element::AxisType, series::{Line, Scatter}, }; @@ -91,9 +91,10 @@ fn LossHistory() -> View { } let chart = Chart::new() .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40).start(0).end(100)) .x_axis(step_axis) .y_axis(scaled_loss_axis) - .grid(Grid::new().top(20).right(40).bottom(30).left(60)) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) .series(scaled_loss_series); renderer.render(CONTAINER_ID, &chart).unwrap(); }); @@ -143,9 +144,10 @@ fn SpectrumHistory() -> View { } let chart = Chart::new() .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40).start(0).end(100)) .x_axis(step_axis) .y_axis(eigval_axis) - .grid(Grid::new().top(20).right(40).bottom(30).left(60)) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) .series(eigval_series); renderer.render(CONTAINER_ID, &chart).unwrap(); }); From fa39a9a97de5f29d131514c452ddcac32c1bf7e8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 12:57:11 -0700 Subject: [PATCH 10/24] 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 --- app-proto/src/diagnostics.rs | 107 +++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 37 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index d283c51..0aa46c3 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -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> { + 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::]); - } + let scaled_loss_series = Line::new().data( + if scaled_loss.len() > 0 { + scaled_loss + } else { + vec![vec![Some(0.0), None::]] + } + ); 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::>() + .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::, None::]); - } + 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::]] + } + ); + 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::]] + } + ); 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(); }); }); From c2c1e4a06d8f802b6d717d603750c85e86ab3fc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 15:44:42 -0700 Subject: [PATCH 11/24] Map diagnostics chart series to log scale by hand This works around problems with the way ECharts does scaling and data zoom for logarithmic axes. --- app-proto/src/diagnostics.rs | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index 0aa46c3..47a9086 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -50,8 +50,11 @@ fn RealizationStatus() -> View { } } -fn into_time_point((step, value): (usize, f64)) -> Vec> { - vec![Some(step as f64), Some(value)] +fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { + vec![ + Some(step as f64), + if value == 0.0 { None } else { Some(value.abs().log10()) } + ] } // the loss history from the last realization @@ -69,15 +72,15 @@ fn LossHistory() -> View { .iter() .enumerate() .map(|(step, &loss)| (step, loss)) - .map(into_time_point) + .map(into_log10_time_point) .collect() ); - // initialize the chart axes and series + // initialize the chart axes let step_axis = Axis::new() .type_(AxisType::Category) .boundary_gap(false); - let scaled_loss_axis = Axis::new().type_(AxisType::Log); + let scaled_loss_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 @@ -116,9 +119,8 @@ fn SpectrumHistory() -> View { on_mount(move || { create_effect(move || { // 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; + // positive and negative parts. exact zero eigenvalues will be + // filtered out by `into_log10_time_point` later let ( hess_eigvals_pos, hess_eigvals_neg @@ -127,22 +129,19 @@ fn SpectrumHistory() -> View { .iter() .enumerate() .map( - |(step, eigvals)| eigvals - .iter() - .filter(|&&val| val.abs() > ZERO_THRESHOLD) - .map( - move |&val| (step, val) - ) + |(step, eigvals)| eigvals.iter().map( + move |&val| (step, val) + ) ) .flatten() .partition(|&(_, val)| val > 0.0) ); - // initialize the chart axes and series + // initialize the chart axes let step_axis = Axis::new() .type_(AxisType::Category) .boundary_gap(false); - let eigval_axis = Axis::new().type_(AxisType::Log); + 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 @@ -154,7 +153,7 @@ fn SpectrumHistory() -> View { if hess_eigvals_pos.len() > 0 { hess_eigvals_pos .into_iter() - .map(into_time_point) + .map(into_log10_time_point) .collect() } else { vec![vec![Some(0.0), None::]] @@ -167,8 +166,7 @@ fn SpectrumHistory() -> View { if hess_eigvals_neg.len() > 0 { hess_eigvals_neg .into_iter() - .map(|(step, val)| (step, -val)) - .map(into_time_point) + .map(into_log10_time_point) .collect() } else { vec![vec![Some(0.0), None::]] From 4765acf6b9d151bbff5b235f6b9bc5738f66dc74 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 18:58:06 -0700 Subject: [PATCH 12/24] Add a strictly-zero series to the spectrum history --- app-proto/src/diagnostics.rs | 38 ++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index 47a9086..a2f090a 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -119,11 +119,10 @@ fn SpectrumHistory() -> View { on_mount(move || { create_effect(move || { // get the spectrum of the Hessian at each step, split into its - // positive and negative parts. exact zero eigenvalues will be - // filtered out by `into_log10_time_point` later + // positive, negative, and strictly-zero parts let ( - hess_eigvals_pos, - hess_eigvals_neg + hess_eigvals_zero, + hess_eigvals_nonzero ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( |history| history.hess_eigvals .iter() @@ -134,8 +133,20 @@ fn SpectrumHistory() -> View { ) ) .flatten() - .partition(|&(_, val)| val > 0.0) + .partition(|&(_, val)| val == 0.0) ); + let zero_level = hess_eigvals_nonzero + .iter() + .map(|(_, val)| val.abs()) + .reduce(f64::min) + .map(|val| 0.1 * val) + .unwrap_or(1.0); + let ( + hess_eigvals_pos, + hess_eigvals_neg + ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero + .into_iter() + .partition(|&(_, val)| val > 0.0); // initialize the chart axes let step_axis = Axis::new() @@ -172,6 +183,20 @@ fn SpectrumHistory() -> View { vec![vec![Some(0.0), None::]] } ); + let eigval_series_zero = Scatter::new() + .symbol(Symbol::Triangle) + .symbol_size(5.0) + .data( + if hess_eigvals_zero.len() > 0 { + hess_eigvals_zero + .into_iter() + .map(|(step, _)| (step, zero_level)) + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); let chart = Chart::new() .animation(false) .data_zoom(DataZoom::new().y_axis_index(0).right(40)) @@ -179,7 +204,8 @@ fn SpectrumHistory() -> View { .y_axis(eigval_axis) .grid(Grid::new().top(20).right(80).bottom(30).left(60)) .series(eigval_series_pos) - .series(eigval_series_neg); + .series(eigval_series_neg) + .series(eigval_series_zero); renderer.render(CONTAINER_ID, &chart).unwrap(); }); }); From 4cb32625556743de30b49eed4b6bfce8ab9190dc Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 26 Jun 2025 22:11:02 +0000 Subject: [PATCH 13/24] chore: Update Sycamore to 0.9.1 (#91) Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/91 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.lock | 25 +++++++++++++------------ app-proto/Cargo.toml | 2 +- app-proto/src/outline.rs | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 3bf609c..55e8686 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -423,9 +423,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "sycamore" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ "hashbrown", "indexmap", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "sycamore-core" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ "hashbrown", "paste", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "sycamore-macro" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137" dependencies = [ "once_cell", "proc-macro2", @@ -465,20 +465,21 @@ dependencies = [ [[package]] name = "sycamore-reactive" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add" dependencies = [ "paste", "slotmap", "smallvec", + "wasm-bindgen", ] [[package]] name = "sycamore-view-parser" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033" dependencies = [ "proc-macro2", "quote", @@ -487,9 +488,9 @@ dependencies = [ [[package]] name = "sycamore-web" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd" dependencies = [ "html-escape", "js-sys", diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 9b46b2b..6932b72 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -15,7 +15,7 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -sycamore = "0.9.0-beta.3" +sycamore = "0.9.1" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 59bbdcc..77d8575 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -151,7 +151,7 @@ fn ElementOutlineItem(element: Rc) -> View { .clone() .into_iter() .sorted_by_key(|reg| reg.subjects().len()) - .collect() + .collect::>() ); let details_node = create_node_ref(); view! { @@ -241,7 +241,7 @@ pub fn Outline() -> View { .clone() .into_iter() .sorted_by_key(|elt| elt.id().clone()) - .collect() + .collect::>() ); view! { From 679c421d04a1c863158e8779ea62c8c464dd2581 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 14/24] Encapsulate realization results In the process, spruce up our realization diagnostics logging and factor out some of the repetitive code in the examples, because we're already changing those parts of the code to adapt them to the new encapsulation. This commit changes the example output format. I've checked by hand that the output is rearranged but not meaningfully changed. --- app-proto/examples/common/mod.rs | 36 +++++++++++++++ app-proto/examples/irisawa-hexlet.rs | 33 +++++++------- app-proto/examples/kaleidocycle.rs | 53 ++++++++++++---------- app-proto/examples/point-on-sphere.rs | 38 +++++++++------- app-proto/examples/three-spheres.rs | 29 ++++++------ app-proto/run-examples | 6 +-- app-proto/src/assembly.rs | 21 +++++---- app-proto/src/engine.rs | 63 ++++++++++++++++----------- 8 files changed, 176 insertions(+), 103 deletions(-) create mode 100644 app-proto/examples/common/mod.rs diff --git a/app-proto/examples/common/mod.rs b/app-proto/examples/common/mod.rs new file mode 100644 index 0000000..b1b91b5 --- /dev/null +++ b/app-proto/examples/common/mod.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use nalgebra::DMatrix; + +use dyna3::engine::{Q, DescentHistory, RealizationResult}; + +pub fn print_title(title: &str) { + println!("─── {title} ───"); +} + +pub fn print_realization_diagnostics(realization_result: &RealizationResult) { + let RealizationResult { result, history } = realization_result; + println!(); + if let Err(ref msg) = result { + println!("❌️ {msg}"); + } else { + println!("✅️ Target accuracy achieved!"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); +} + +pub fn print_gram_matrix(config: &DMatrix) { + println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); +} + +pub fn print_config(config: &DMatrix) { + println!("\nConfiguration:{}", config.to_string().trim_end()); +} + +pub fn print_loss_history(history: &DescentHistory) { + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 639a494..750a0d0 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,25 +1,28 @@ -use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; +mod common; + +use common::{ + print_gram_matrix, + print_loss_history, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{Realization, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + let realization_result = realize_irisawa_hexlet(SCALED_TOL); + print_title("Irisawa hexlet"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization { config, .. }) = realization_result.result { + // print the diameters of the chain spheres println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } + + // print the completed Gram matrix + print_gram_matrix(&config); } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } + print_loss_history(&realization_result.history); } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 2779ab1..21f8187 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,30 +1,37 @@ +mod common; + use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{Q, examples::realize_kaleidocycle}; +use common::{ + print_config, + print_gram_matrix, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{Realization, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); + let realization_result = realize_kaleidocycle(SCALED_TOL); + print_title("Kaleidocycle"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization { config, tangent }) = realization_result.result { + // print the completed Gram matrix and the realized configuration + print_gram_matrix(&config); + print_config(&config); + + // find the kaleidocycle's twist motion by projecting onto the tangent + // space + const N_POINTS: usize = 12; + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - - // find the kaleidocycle's twist motion by projecting onto the tangent space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1) - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - print!("Twist motion:{}", normalization * twist_motion); } \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 880d7b0..774368e 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,4 +1,19 @@ -use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; +mod common; + +use common::{ + print_config, + print_gram_matrix, + print_loss_history, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{ + point, + realize_gram, + sphere, + ConstraintProblem, + Realization +}; fn main() { let mut problem = ConstraintProblem::from_guess(&[ @@ -11,21 +26,14 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - println!(); - let (config, _, success, history) = realize_gram( + let realization_result = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print_title("Point on a sphere"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization{ config, .. }) = realization_result.result { + print_gram_matrix(&config); + print_config(&config); } + print_loss_history(&realization_result.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 3f3cc44..dd2cdc0 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,4 +1,12 @@ -use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; +mod common; + +use common::{ + print_gram_matrix, + print_loss_history, + print_realization_diagnostics, + print_title +}; +use dyna3::engine::{realize_gram, sphere, ConstraintProblem, Realization}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -14,20 +22,13 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - println!(); - let (config, _, success, history) = realize_gram( + let realization_result = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print_title("Three spheres"); + print_realization_diagnostics(&realization_result); + if let Ok(Realization{ config, .. }) = realization_result.result { + print_gram_matrix(&config); } + print_loss_history(&realization_result.history); } \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index 52173b0..b3e3121 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -6,7 +6,7 @@ # http://xion.io/post/code/rust-examples.html # -cargo run --example irisawa-hexlet -cargo run --example three-spheres -cargo run --example point-on-sphere +cargo run --example irisawa-hexlet; echo +cargo run --example three-spheres; echo +cargo run --example point-on-sphere; echo cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 6c91fc0..9f66055 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -24,7 +24,9 @@ use crate::{ realize_gram, sphere, ConfigSubspace, - ConstraintProblem + ConstraintProblem, + Realization, + RealizationResult }, outline::OutlineItem, specified::SpecifiedValue @@ -687,22 +689,25 @@ impl Assembly { console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix - let (config, tangent, success, history) = realize_gram( + let RealizationResult { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ // report the outcome of the search - if success { - console_log!("Target accuracy achieved!") + if let Err(ref msg) = result { + console_log!("❌️ {msg}"); } else { - console_log!("Failed to reach target accuracy") + console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); - console_log!("Tangent dimension: {}", tangent.dim()); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + if let Ok(Realization { config, tangent }) = result { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + // read out the solution for elt in self.elements.get_clone_untracked() { elt.representation().update( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index c5d7b00..2524fcd 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -393,6 +393,16 @@ fn seek_better_config( None } +pub struct Realization { + pub config: DMatrix, + pub tangent: ConfigSubspace +} + +pub struct RealizationResult { + pub result: Result, + pub history: DescentHistory +} + // seek a matrix `config` that matches the partial matrix `problem.frozen` and // has `config' * Q * config` matching the partial matrix `problem.gram`. start // at `problem.guess`, set the frozen entries to their desired values, and then @@ -405,7 +415,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { +) -> RealizationResult { // destructure the problem data let ConstraintProblem { gram, guess, frozen @@ -491,19 +501,20 @@ pub fn realize_gram( history.base_step.push(base_step.clone()); // use backtracking line search to find a better configuration - match seek_better_config( + if let Some((better_state, backoff_steps)) = seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), min_efficiency, backoff, max_backoff_steps ) { - Some((better_state, backoff_steps)) => { - state = better_state; - history.backoff_steps.push(backoff_steps); - }, - None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) + state = better_state; + history.backoff_steps.push(backoff_steps); + } else { + return RealizationResult { + result: Err("Line search failed".to_string()), + history + } }; } - let success = state.loss < tol; - let tangent = if success { + let result = if state.loss < tol { // express the uniform basis in the standard basis const UNIFORM_DIM: usize = 4; let total_dim_unif = UNIFORM_DIM * assembly_dim; @@ -516,11 +527,13 @@ pub fn realize_gram( } // find the kernel of the Hessian. give it the uniform inner product - ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) + let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); + + Ok(Realization { config: state.config, tangent }) } else { - ConfigSubspace::zero(assembly_dim) + Err("Failed to reach target accuracy".to_string()) }; - (state.config, tangent, success, history) + RealizationResult{ result, history } } // --- tests --- @@ -539,7 +552,7 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> RealizationResult { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -590,7 +603,7 @@ pub mod examples { // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space - pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_kaleidocycle(scaled_tol: f64) -> RealizationResult { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -714,10 +727,10 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let (config, _, success, history) = realize_gram( + let RealizationResult { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(success, true); + let config = result.unwrap().config; for base_step in history.base_step.into_iter() { for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); @@ -732,7 +745,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -759,11 +772,11 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let (config, tangent, success, history) = realize_gram( + let RealizationResult { result, history } = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let Realization { config, tangent } = result.unwrap(); assert_eq!(config, problem.guess); - assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -831,8 +844,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - assert_eq!(success, true); + let RealizationResult { result, history } = realize_kaleidocycle(SCALED_TOL); + let Realization { config, tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -920,11 +933,11 @@ mod tests { problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + let RealizationResult { result: result_orig, history: history_orig } = realize_gram( &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let Realization { config: config_orig, tangent: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); - assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); // find another pair of spheres that meet at 120°. we'll think of this @@ -941,11 +954,11 @@ mod tests { guess: guess_tfm, frozen: problem_orig.frozen }; - let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + let RealizationResult { result: result_tfm, history: history_tfm } = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let Realization { config: config_tfm, tangent: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); - assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); // project a nudge to the tangent space of the solution variety at the From 0b333ac00df78c5ede72b95fad43240ee53aee93 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 15/24] Show the loss history from the last realization This introduces a dependency on the Charming crate, which we use to plot the loss history, and the ECharts JavaScript library, which Charming depends on. Now that there's more than one canvas on the page, we have to pick out the display by ID rather than by element type in our style sheet. --- app-proto/Cargo.lock | 565 ++++++++++++++++++++++++++++++++++- app-proto/Cargo.toml | 3 + app-proto/index.html | 6 + app-proto/main.css | 10 +- app-proto/src/add_remove.rs | 4 +- app-proto/src/assembly.rs | 12 +- app-proto/src/diagnostics.rs | 65 ++++ app-proto/src/display.rs | 1 + app-proto/src/engine.rs | 2 +- app-proto/src/main.rs | 3 + 10 files changed, 660 insertions(+), 11 deletions(-) create mode 100644 app-proto/src/diagnostics.rs diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 55e8686..4f75c45 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -20,6 +20,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "approx" version = "0.5.1" @@ -35,6 +50,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -68,6 +98,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charming" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09" +dependencies = [ + "handlebars", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_with", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -78,10 +137,122 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyna3" version = "0.1.0" dependencies = [ + "charming", "console_error_panic_hook", "dyna3", "itertools", @@ -106,6 +277,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -117,6 +304,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -127,6 +336,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html-escape" version = "0.2.13" @@ -136,6 +351,47 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -143,7 +399,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -155,6 +412,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.70" @@ -192,6 +455,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minicov" version = "0.3.5" @@ -248,6 +517,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -257,6 +532,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -289,6 +579,57 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -363,6 +704,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.2" @@ -387,6 +734,90 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -421,14 +852,20 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "sycamore" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ - "hashbrown", - "indexmap", + "hashbrown 0.14.5", + "indexmap 2.5.0", "paste", "sycamore-core", "sycamore-macro", @@ -444,7 +881,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "paste", "sycamore-reactive", ] @@ -506,21 +943,78 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -677,6 +1171,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 6932b72..1230b47 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -17,6 +17,9 @@ nalgebra = "0.33.0" readonly = "0.2.12" sycamore = "0.9.1" +# We use Charming to help display engine diagnostics +charming = { version = "0.5.1", features = ["wasm"] } + # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for diff --git a/app-proto/index.html b/app-proto/index.html index 92238f4..4fbe52f 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,6 +6,12 @@ + + + diff --git a/app-proto/main.css b/app-proto/main.css index d56784f..fb16698 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -172,9 +172,17 @@ details[open]:has(li) .element-switch::after { color: var(--text-invalid); } +/* diagnostics */ + +#loss-history { + margin: 10px; + background-color: var(--display-background); + border-radius: 8px; +} + /* display */ -canvas { +#display { float: left; margin-left: 20px; margin-top: 20px; diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index f3bbc97..d737c79 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -3,8 +3,9 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ - engine, AppState, + engine, + engine::DescentHistory, assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; @@ -195,6 +196,7 @@ pub fn AddRemove() -> View { assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); state.selection.update(|sel| sel.clear()); // load assembly diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 9f66055..f466108 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -25,6 +25,7 @@ use crate::{ sphere, ConfigSubspace, ConstraintProblem, + DescentHistory, Realization, RealizationResult }, @@ -549,7 +550,10 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal>> + pub elements_by_id: Signal>>, + + // realization diagnostics + pub descent_history: Signal } impl Assembly { @@ -558,7 +562,8 @@ impl Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()) + elements_by_id: create_signal(BTreeMap::default()), + descent_history: create_signal(DescentHistory::new()) } } @@ -703,6 +708,9 @@ impl Assembly { console_log!("Steps: {}", history.scaled_loss.len() - 1); console_log!("Loss: {}", history.scaled_loss.last().unwrap()); + // record realization diagnostics + self.descent_history.set(history); + if let Ok(Realization { config, tangent }) = result { /* DEBUG */ // report the tangent dimension diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs new file mode 100644 index 0000000..6d3d145 --- /dev/null +++ b/app-proto/src/diagnostics.rs @@ -0,0 +1,65 @@ +use charming::{ + Chart, + WasmRenderer, + component::{Axis, Grid}, + element::AxisType, + series::Line, + theme::Theme +}; +use sycamore::prelude::*; + +use crate::AppState; + +// a plot of the loss history from the last realization +#[component] +pub fn Diagnostics() -> View { + const CONTAINER_ID: &str = "loss-history"; + let state = use_context::(); + let renderer = WasmRenderer::new_opt(None, Some(180)).theme(Theme::Walden); + + on_mount(move || { + create_effect(move || { + // get the loss history + let scaled_loss = state.assembly.descent_history.with( + |history| history.scaled_loss.clone() + ); + let step_cnt = scaled_loss.len(); + + // initialize the chart axes and series + const MIN_INTERVAL: f64 = 0.01; + let mut 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); + + // 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::]); + } + let chart = Chart::new() + .animation(false) + .x_axis(step_axis) + .y_axis(scaled_loss_axis) + .grid(Grid::new().top(20).right(40).bottom(30).left(60)) + .series(scaled_loss_series); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID) + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 69a3659..1646c4e 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -806,6 +806,7 @@ pub fn Display() -> View { // again canvas( ref=display, + id="display", width="600", height="600", tabindex="0", diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 2524fcd..38003c9 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -262,7 +262,7 @@ pub struct DescentHistory { } impl DescentHistory { - fn new() -> DescentHistory { + pub fn new() -> DescentHistory { DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index b76859a..f905c46 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,5 +1,6 @@ mod add_remove; mod assembly; +mod diagnostics; mod display; mod engine; mod outline; @@ -13,6 +14,7 @@ use sycamore::prelude::*; use add_remove::AddRemove; use assembly::{Assembly, Element}; +use diagnostics::Diagnostics; use display::Display; use outline::Outline; @@ -60,6 +62,7 @@ fn main() { div(id="sidebar") { AddRemove {} Outline {} + Diagnostics {} } Display {} } From 402f5609c0af9fb54eb038e8cefffb259463a121 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 16/24] Add a realization status indicator --- app-proto/examples/common/mod.rs | 4 +-- app-proto/main.css | 42 ++++++++++++++++++++++------ app-proto/src/assembly.rs | 48 +++++++++++++++++++++----------- app-proto/src/diagnostics.rs | 40 +++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 29 deletions(-) diff --git a/app-proto/examples/common/mod.rs b/app-proto/examples/common/mod.rs index b1b91b5..9b6c492 100644 --- a/app-proto/examples/common/mod.rs +++ b/app-proto/examples/common/mod.rs @@ -11,8 +11,8 @@ pub fn print_title(title: &str) { pub fn print_realization_diagnostics(realization_result: &RealizationResult) { let RealizationResult { result, history } = realization_result; println!(); - if let Err(ref msg) = result { - println!("❌️ {msg}"); + if let Err(ref message) = result { + println!("❌️ {message}"); } else { println!("✅️ Target accuracy achieved!"); } diff --git a/app-proto/main.css b/app-proto/main.css index fb16698..a73d5a5 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -18,6 +18,17 @@ body { font-family: 'Fira Sans', sans-serif; } +.invalid { + color: var(--text-invalid); +} + +.status { + width: 20px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + /* sidebar */ #sidebar { @@ -138,6 +149,7 @@ details[open]:has(li) .element-switch::after { } .regulator-input { + margin-right: 4px; color: inherit; background-color: inherit; border: 1px solid var(--border); @@ -159,14 +171,6 @@ details[open]:has(li) .element-switch::after { border-color: var(--border-invalid); } -.status { - width: 20px; - padding-left: 4px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); @@ -174,8 +178,28 @@ details[open]:has(li) .element-switch::after { /* diagnostics */ -#loss-history { +#diagnostics { margin: 10px; +} + +#realization-status { + display: flex; +} + +#realization-status .status { + margin-right: 4px; +} + +#realization-status .status::after { + content: '✓'; +} + +#realization-status.invalid .status::after { + content: '⚠'; +} + +#loss-history { + margin-top: 10px; background-color: var(--display-background); border-radius: 8px; } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index f466108..9a4b934 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -553,6 +553,7 @@ pub struct Assembly { pub elements_by_id: Signal>>, // realization diagnostics + pub realization_status: Signal>, pub descent_history: Signal } @@ -563,6 +564,7 @@ impl Assembly { regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(BTreeMap::default()), + realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()) } } @@ -699,32 +701,44 @@ impl Assembly { ); /* DEBUG */ - // report the outcome of the search - if let Err(ref msg) = result { - console_log!("❌️ {msg}"); + // report the outcome of the search in the browser console + if let Err(ref message) = result { + console_log!("❌️ {message}"); } else { console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - // record realization diagnostics + // report the loss history self.descent_history.set(history); - if let Ok(Realization { config, tangent }) = result { - /* DEBUG */ - // report the tangent dimension - console_log!("Tangent dimension: {}", tangent.dim()); - - // 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())) - ); + match result { + Ok(Realization { config, tangent }) => { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + + // 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())) + ); + } + + // save the tangent space + self.tangent.set_silent(tangent); + }, + Err(message) => { + // report the realization status. the `Err(message)` we're + // 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)) } - - // save the tangent space - self.tangent.set_silent(tangent); } } diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index 6d3d145..a73a793 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -10,9 +10,37 @@ use sycamore::prelude::*; use crate::AppState; +// a realization status indicator +#[component] +pub fn RealizationStatus() -> View { + let state = use_context::(); + let realization_status = state.assembly.realization_status; + view! { + div( + id="realization-status", + class=realization_status.with( + |status| match status { + Ok(_) => "", + Err(_) => "invalid" + } + ) + ) { + div(class="status") + div { + (realization_status.with( + |status| match status { + Ok(_) => "Target accuracy achieved".to_string(), + Err(message) => message.clone() + } + )) + } + } + } +} + // a plot of the loss history from the last realization #[component] -pub fn Diagnostics() -> View { +pub fn LossHistory() -> View { const CONTAINER_ID: &str = "loss-history"; let state = use_context::(); let renderer = WasmRenderer::new_opt(None, Some(180)).theme(Theme::Walden); @@ -62,4 +90,14 @@ pub fn Diagnostics() -> View { view! { div(id=CONTAINER_ID) } +} + +#[component] +pub fn Diagnostics() -> View { + view! { + div(id="diagnostics") { + RealizationStatus {} + LossHistory {} + } + } } \ No newline at end of file From c3c665f35c89a2f7a9ab7a3e40be696a0bcba58f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 17/24] Let the Cholesky decomposition fail gracefully --- app-proto/src/engine.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 38003c9..6d20df6 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -490,13 +490,22 @@ pub fn realize_gram( if state.loss < tol { break; } // compute the Newton step + /* TO DO */ /* - we need to either handle or eliminate the case where the minimum - eigenvalue of the Hessian is zero, so the regularized Hessian is - singular. right now, this causes the Cholesky decomposition to return - `None`, leading to a panic when we unrap + we should change our regularization to ensure that the Hessian is + is positive-definite, rather than just positive-semidefinite. ideally, + that would guarantee the success of the Cholesky decomposition--- + although we'd still need the error-handling routine in case of + numerical hiccups */ - let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); + let hess_cholesky = match hess.clone().cholesky() { + Some(cholesky) => cholesky, + None => return RealizationResult { + result: Err("Cholesky decomposition failed".to_string()), + history + } + }; + let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); From 28cf19cd260a2bffb87ec2a2eefe7412e72dc741 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 18/24] Fix the display's focus indicator --- app-proto/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/main.css b/app-proto/main.css index a73d5a5..8b96942 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -215,7 +215,7 @@ details[open]:has(li) .element-switch::after { border-radius: 16px; } -canvas:focus { +#display:focus { border-color: var(--border-focus-dark); outline: none; } From 652cdd15737a111e7a87a6542daec1a3bf5f291d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 19/24] Put a border around the loss history chart --- app-proto/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app-proto/main.css b/app-proto/main.css index 8b96942..14276dd 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -201,6 +201,7 @@ details[open]:has(li) .element-switch::after { #loss-history { margin-top: 10px; background-color: var(--display-background); + border: 1px solid var(--border); border-radius: 8px; } From de844cb63b96821d4e7119816e7af143a3fdfa4e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 11 Jun 2025 01:21:18 -0700 Subject: [PATCH 20/24] 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 From af28e885bb8b1ab58c72d25a933c88524b512424 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 17 Jun 2025 14:07:51 -0700 Subject: [PATCH 21/24] Add data zoom controls to the diagnostics charts --- app-proto/src/diagnostics.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index b90989f..d283c51 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -1,7 +1,7 @@ use charming::{ Chart, WasmRenderer, - component::{Axis, Grid}, + component::{Axis, DataZoom, Grid}, element::AxisType, series::{Line, Scatter}, }; @@ -91,9 +91,10 @@ fn LossHistory() -> View { } let chart = Chart::new() .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40).start(0).end(100)) .x_axis(step_axis) .y_axis(scaled_loss_axis) - .grid(Grid::new().top(20).right(40).bottom(30).left(60)) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) .series(scaled_loss_series); renderer.render(CONTAINER_ID, &chart).unwrap(); }); @@ -143,9 +144,10 @@ fn SpectrumHistory() -> View { } let chart = Chart::new() .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40).start(0).end(100)) .x_axis(step_axis) .y_axis(eigval_axis) - .grid(Grid::new().top(20).right(40).bottom(30).left(60)) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) .series(eigval_series); renderer.render(CONTAINER_ID, &chart).unwrap(); }); From 2688b76678ba16aabd830755fd69b5d6cedce3eb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 12:57:11 -0700 Subject: [PATCH 22/24] 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 --- app-proto/src/diagnostics.rs | 107 +++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 37 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index d283c51..0aa46c3 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -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> { + 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::]); - } + let scaled_loss_series = Line::new().data( + if scaled_loss.len() > 0 { + scaled_loss + } else { + vec![vec![Some(0.0), None::]] + } + ); 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::>() + .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::, None::]); - } + 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::]] + } + ); + 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::]] + } + ); 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(); }); }); From e8779852021035fc5758808180e88817b9b410a6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 15:44:42 -0700 Subject: [PATCH 23/24] Map diagnostics chart series to log scale by hand This works around problems with the way ECharts does scaling and data zoom for logarithmic axes. --- app-proto/src/diagnostics.rs | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index 0aa46c3..47a9086 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -50,8 +50,11 @@ fn RealizationStatus() -> View { } } -fn into_time_point((step, value): (usize, f64)) -> Vec> { - vec![Some(step as f64), Some(value)] +fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { + vec![ + Some(step as f64), + if value == 0.0 { None } else { Some(value.abs().log10()) } + ] } // the loss history from the last realization @@ -69,15 +72,15 @@ fn LossHistory() -> View { .iter() .enumerate() .map(|(step, &loss)| (step, loss)) - .map(into_time_point) + .map(into_log10_time_point) .collect() ); - // initialize the chart axes and series + // initialize the chart axes let step_axis = Axis::new() .type_(AxisType::Category) .boundary_gap(false); - let scaled_loss_axis = Axis::new().type_(AxisType::Log); + let scaled_loss_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 @@ -116,9 +119,8 @@ fn SpectrumHistory() -> View { on_mount(move || { create_effect(move || { // 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; + // positive and negative parts. exact zero eigenvalues will be + // filtered out by `into_log10_time_point` later let ( hess_eigvals_pos, hess_eigvals_neg @@ -127,22 +129,19 @@ fn SpectrumHistory() -> View { .iter() .enumerate() .map( - |(step, eigvals)| eigvals - .iter() - .filter(|&&val| val.abs() > ZERO_THRESHOLD) - .map( - move |&val| (step, val) - ) + |(step, eigvals)| eigvals.iter().map( + move |&val| (step, val) + ) ) .flatten() .partition(|&(_, val)| val > 0.0) ); - // initialize the chart axes and series + // initialize the chart axes let step_axis = Axis::new() .type_(AxisType::Category) .boundary_gap(false); - let eigval_axis = Axis::new().type_(AxisType::Log); + 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 @@ -154,7 +153,7 @@ fn SpectrumHistory() -> View { if hess_eigvals_pos.len() > 0 { hess_eigvals_pos .into_iter() - .map(into_time_point) + .map(into_log10_time_point) .collect() } else { vec![vec![Some(0.0), None::]] @@ -167,8 +166,7 @@ fn SpectrumHistory() -> View { if hess_eigvals_neg.len() > 0 { hess_eigvals_neg .into_iter() - .map(|(step, val)| (step, -val)) - .map(into_time_point) + .map(into_log10_time_point) .collect() } else { vec![vec![Some(0.0), None::]] From 68d6cc164592e551daef3a8d3f5fe87ac3c665d2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 18:58:06 -0700 Subject: [PATCH 24/24] Add a strictly-zero series to the spectrum history --- app-proto/src/diagnostics.rs | 38 ++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs index 47a9086..a2f090a 100644 --- a/app-proto/src/diagnostics.rs +++ b/app-proto/src/diagnostics.rs @@ -119,11 +119,10 @@ fn SpectrumHistory() -> View { on_mount(move || { create_effect(move || { // get the spectrum of the Hessian at each step, split into its - // positive and negative parts. exact zero eigenvalues will be - // filtered out by `into_log10_time_point` later + // positive, negative, and strictly-zero parts let ( - hess_eigvals_pos, - hess_eigvals_neg + hess_eigvals_zero, + hess_eigvals_nonzero ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( |history| history.hess_eigvals .iter() @@ -134,8 +133,20 @@ fn SpectrumHistory() -> View { ) ) .flatten() - .partition(|&(_, val)| val > 0.0) + .partition(|&(_, val)| val == 0.0) ); + let zero_level = hess_eigvals_nonzero + .iter() + .map(|(_, val)| val.abs()) + .reduce(f64::min) + .map(|val| 0.1 * val) + .unwrap_or(1.0); + let ( + hess_eigvals_pos, + hess_eigvals_neg + ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero + .into_iter() + .partition(|&(_, val)| val > 0.0); // initialize the chart axes let step_axis = Axis::new() @@ -172,6 +183,20 @@ fn SpectrumHistory() -> View { vec![vec![Some(0.0), None::]] } ); + let eigval_series_zero = Scatter::new() + .symbol(Symbol::Triangle) + .symbol_size(5.0) + .data( + if hess_eigvals_zero.len() > 0 { + hess_eigvals_zero + .into_iter() + .map(|(step, _)| (step, zero_level)) + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); let chart = Chart::new() .animation(false) .data_zoom(DataZoom::new().y_axis_index(0).right(40)) @@ -179,7 +204,8 @@ fn SpectrumHistory() -> View { .y_axis(eigval_axis) .grid(Grid::new().top(20).right(80).bottom(30).left(60)) .series(eigval_series_pos) - .series(eigval_series_neg); + .series(eigval_series_neg) + .series(eigval_series_zero); renderer.render(CONTAINER_ID, &chart).unwrap(); }); });