From 679c421d04a1c863158e8779ea62c8c464dd2581 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 01/16] 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 -- 2.43.0 From 0b333ac00df78c5ede72b95fad43240ee53aee93 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 02/16] 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 {} } -- 2.43.0 From 402f5609c0af9fb54eb038e8cefffb259463a121 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 03/16] 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 -- 2.43.0 From c3c665f35c89a2f7a9ab7a3e40be696a0bcba58f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 04/16] 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()); -- 2.43.0 From 28cf19cd260a2bffb87ec2a2eefe7412e72dc741 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 05/16] 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; } -- 2.43.0 From 652cdd15737a111e7a87a6542daec1a3bf5f291d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Jun 2025 22:21:34 -0700 Subject: [PATCH 06/16] 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; } -- 2.43.0 From de844cb63b96821d4e7119816e7af143a3fdfa4e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 11 Jun 2025 01:21:18 -0700 Subject: [PATCH 07/16] 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 -- 2.43.0 From af28e885bb8b1ab58c72d25a933c88524b512424 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 17 Jun 2025 14:07:51 -0700 Subject: [PATCH 08/16] 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(); }); -- 2.43.0 From 2688b76678ba16aabd830755fd69b5d6cedce3eb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 12:57:11 -0700 Subject: [PATCH 09/16] 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(); }); }); -- 2.43.0 From e8779852021035fc5758808180e88817b9b410a6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 15:44:42 -0700 Subject: [PATCH 10/16] 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::]] -- 2.43.0 From 68d6cc164592e551daef3a8d3f5fe87ac3c665d2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Jun 2025 18:58:06 -0700 Subject: [PATCH 11/16] 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(); }); }); -- 2.43.0 From 213728435827a08fd306a93d63dc06b105af0cb2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 15 Jul 2025 21:18:17 -0700 Subject: [PATCH 12/16] Give the run-examples script a shell extension Also make the script non-executable, so it has to be run with a command like `sh run-examples.sh`. This makes the shebang line superfluous. --- app-proto/{run-examples => run-examples.sh} | 2 -- 1 file changed, 2 deletions(-) rename app-proto/{run-examples => run-examples.sh} (86%) mode change 100755 => 100644 diff --git a/app-proto/run-examples b/app-proto/run-examples.sh old mode 100755 new mode 100644 similarity index 86% rename from app-proto/run-examples rename to app-proto/run-examples.sh index b3e3121..50fafbe --- a/app-proto/run-examples +++ b/app-proto/run-examples.sh @@ -1,5 +1,3 @@ -#!/bin/sh - # run all Cargo examples, as described here: # # Karol Kuczmarski. "Add examples to your Rust libraries" -- 2.43.0 From 477d6a506446add601141627978891ce27ff3390 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 18 Jul 2025 10:59:41 -0700 Subject: [PATCH 13/16] Reorganize the shared example code The new layout deviates from what the Rust book suggests https://doc.rust-lang.org/book/ch11-03-test-organization.html#submodules-in-integration-tests and uses the frowned-upon `#[path]` attribute, https://doc.rust-lang.org/style-guide/advice.html#modules but we've decided that having a descriptive module filename instead of `mod.rs` is worth the cost. --- .../examples/common/{mod.rs => print.rs} | 10 +++++----- app-proto/examples/irisawa-hexlet.rs | 17 ++++++---------- app-proto/examples/kaleidocycle.rs | 17 ++++++---------- app-proto/examples/point-on-sphere.rs | 20 +++++++------------ app-proto/examples/three-spheres.rs | 17 ++++++---------- 5 files changed, 30 insertions(+), 51 deletions(-) rename app-proto/examples/common/{mod.rs => print.rs} (78%) diff --git a/app-proto/examples/common/mod.rs b/app-proto/examples/common/print.rs similarity index 78% rename from app-proto/examples/common/mod.rs rename to app-proto/examples/common/print.rs index 9b6c492..35857c5 100644 --- a/app-proto/examples/common/mod.rs +++ b/app-proto/examples/common/print.rs @@ -4,11 +4,11 @@ use nalgebra::DMatrix; use dyna3::engine::{Q, DescentHistory, RealizationResult}; -pub fn print_title(title: &str) { +pub fn title(title: &str) { println!("─── {title} ───"); } -pub fn print_realization_diagnostics(realization_result: &RealizationResult) { +pub fn realization_diagnostics(realization_result: &RealizationResult) { let RealizationResult { result, history } = realization_result; println!(); if let Err(ref message) = result { @@ -20,15 +20,15 @@ pub fn print_realization_diagnostics(realization_result: &RealizationResult) { println!("Loss: {}", history.scaled_loss.last().unwrap()); } -pub fn print_gram_matrix(config: &DMatrix) { +pub fn gram_matrix(config: &DMatrix) { println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); } -pub fn print_config(config: &DMatrix) { +pub fn config(config: &DMatrix) { println!("\nConfiguration:{}", config.to_string().trim_end()); } -pub fn print_loss_history(history: &DescentHistory) { +pub fn loss_history(history: &DescentHistory) { println!("\nStep │ Loss\n─────┼────────────────────────────────"); for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { println!("{:<4} │ {}", step, scaled_loss); diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 750a0d0..ee4a5df 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,18 +1,13 @@ -mod common; +#[path = "common/print.rs"] +mod print; -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 realization_result = realize_irisawa_hexlet(SCALED_TOL); - print_title("Irisawa hexlet"); - print_realization_diagnostics(&realization_result); + 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:"); @@ -22,7 +17,7 @@ fn main() { } // print the completed Gram matrix - print_gram_matrix(&config); + print::gram_matrix(&config); } - print_loss_history(&realization_result.history); + 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 21f8187..b73ecd0 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,24 +1,19 @@ -mod common; +#[path = "common/print.rs"] +mod print; use nalgebra::{DMatrix, DVector}; -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 realization_result = realize_kaleidocycle(SCALED_TOL); - print_title("Kaleidocycle"); - print_realization_diagnostics(&realization_result); + 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); + print::gram_matrix(&config); + print::config(&config); // find the kaleidocycle's twist motion by projecting onto the tangent // space diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 774368e..444fb02 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,12 +1,6 @@ -mod common; +#[path = "common/print.rs"] +mod print; -use common::{ - print_config, - print_gram_matrix, - print_loss_history, - print_realization_diagnostics, - print_title -}; use dyna3::engine::{ point, realize_gram, @@ -29,11 +23,11 @@ fn main() { let realization_result = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print_title("Point on a sphere"); - print_realization_diagnostics(&realization_result); + 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::gram_matrix(&config); + print::config(&config); } - print_loss_history(&realization_result.history); + 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 dd2cdc0..74caf4a 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,11 +1,6 @@ -mod common; +#[path = "common/print.rs"] +mod print; -use common::{ - print_gram_matrix, - print_loss_history, - print_realization_diagnostics, - print_title -}; use dyna3::engine::{realize_gram, sphere, ConstraintProblem, Realization}; fn main() { @@ -25,10 +20,10 @@ fn main() { let realization_result = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print_title("Three spheres"); - print_realization_diagnostics(&realization_result); + print::title("Three spheres"); + print::realization_diagnostics(&realization_result); if let Ok(Realization{ config, .. }) = realization_result.result { - print_gram_matrix(&config); + print::gram_matrix(&config); } - print_loss_history(&realization_result.history); + print::loss_history(&realization_result.history); } \ No newline at end of file -- 2.43.0 From f1865f85a17b6a1cc7ffb56f37134b7f4feaed62 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 18 Jul 2025 12:16:40 -0700 Subject: [PATCH 14/16] Improve naming in realization output structures --- app-proto/examples/common/print.rs | 6 ++-- app-proto/examples/irisawa-hexlet.rs | 10 +++---- app-proto/examples/kaleidocycle.rs | 8 +++--- app-proto/examples/point-on-sphere.rs | 12 ++++---- app-proto/examples/three-spheres.rs | 15 ++++++---- app-proto/src/assembly.rs | 8 +++--- app-proto/src/engine.rs | 41 ++++++++++++++------------- 7 files changed, 53 insertions(+), 47 deletions(-) diff --git a/app-proto/examples/common/print.rs b/app-proto/examples/common/print.rs index 35857c5..2aa6a39 100644 --- a/app-proto/examples/common/print.rs +++ b/app-proto/examples/common/print.rs @@ -2,14 +2,14 @@ use nalgebra::DMatrix; -use dyna3::engine::{Q, DescentHistory, RealizationResult}; +use dyna3::engine::{Q, DescentHistory, Realization}; pub fn title(title: &str) { println!("─── {title} ───"); } -pub fn realization_diagnostics(realization_result: &RealizationResult) { - let RealizationResult { result, history } = realization_result; +pub fn realization_diagnostics(realization: &Realization) { + let Realization { result, history } = realization; println!(); if let Err(ref message) = result { println!("❌️ {message}"); diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index ee4a5df..0d710ff 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,14 +1,14 @@ #[path = "common/print.rs"] mod print; -use dyna3::engine::{Realization, examples::realize_irisawa_hexlet}; +use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let realization_result = realize_irisawa_hexlet(SCALED_TOL); + let realization = realize_irisawa_hexlet(SCALED_TOL); print::title("Irisawa hexlet"); - print::realization_diagnostics(&realization_result); - if let Ok(Realization { config, .. }) = realization_result.result { + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, .. }) = realization.result { // print the diameters of the chain spheres println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); @@ -19,5 +19,5 @@ fn main() { // print the completed Gram matrix print::gram_matrix(&config); } - print::loss_history(&realization_result.history); + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index b73ecd0..7ca1f97 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -3,14 +3,14 @@ mod print; use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{Realization, examples::realize_kaleidocycle}; +use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let realization_result = realize_kaleidocycle(SCALED_TOL); + let realization = realize_kaleidocycle(SCALED_TOL); print::title("Kaleidocycle"); - print::realization_diagnostics(&realization_result); - if let Ok(Realization { config, tangent }) = realization_result.result { + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { // print the completed Gram matrix and the realized configuration print::gram_matrix(&config); print::config(&config); diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 444fb02..89dee76 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -5,8 +5,8 @@ use dyna3::engine::{ point, realize_gram, sphere, - ConstraintProblem, - Realization + ConfigNeighborhood, + ConstraintProblem }; fn main() { @@ -20,14 +20,14 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - let realization_result = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print::title("Point on a sphere"); - print::realization_diagnostics(&realization_result); - if let Ok(Realization{ config, .. }) = realization_result.result { + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { print::gram_matrix(&config); print::config(&config); } - print::loss_history(&realization_result.history); + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 74caf4a..aa5a105 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,7 +1,12 @@ #[path = "common/print.rs"] mod print; -use dyna3::engine::{realize_gram, sphere, ConstraintProblem, Realization}; +use dyna3::engine::{ + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -17,13 +22,13 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - let realization_result = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print::title("Three spheres"); - print::realization_diagnostics(&realization_result); - if let Ok(Realization{ config, .. }) = realization_result.result { + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { print::gram_matrix(&config); } - print::loss_history(&realization_result.history); + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 9a4b934..c3b0c6b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -23,11 +23,11 @@ use crate::{ project_sphere_to_normalized, realize_gram, sphere, + ConfigNeighborhood, ConfigSubspace, ConstraintProblem, DescentHistory, - Realization, - RealizationResult + Realization }, outline::OutlineItem, specified::SpecifiedValue @@ -696,7 +696,7 @@ impl Assembly { console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix - let RealizationResult { result, history } = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); @@ -714,7 +714,7 @@ impl Assembly { self.descent_history.set(history); match result { - Ok(Realization { config, tangent }) => { + Ok(ConfigNeighborhood { config, nbhd: tangent }) => { /* DEBUG */ // report the tangent dimension console_log!("Tangent dimension: {}", tangent.dim()); diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index b6b1e50..e6ffa25 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -393,13 +393,14 @@ fn seek_better_config( None } -pub struct Realization { +// a first-order neighborhood of a configuration +pub struct ConfigNeighborhood { pub config: DMatrix, - pub tangent: ConfigSubspace + pub nbhd: ConfigSubspace } -pub struct RealizationResult { - pub result: Result, +pub struct Realization { + pub result: Result, pub history: DescentHistory } @@ -415,7 +416,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> RealizationResult { +) -> Realization { // destructure the problem data let ConstraintProblem { gram, guess, frozen @@ -501,7 +502,7 @@ pub fn realize_gram( */ let hess_cholesky = match hess.clone().cholesky() { Some(cholesky) => cholesky, - None => return RealizationResult { + None => return Realization { result: Err("Cholesky decomposition failed".to_string()), history } @@ -518,7 +519,7 @@ pub fn realize_gram( state = better_state; history.backoff_steps.push(backoff_steps); } else { - return RealizationResult { + return Realization { result: Err("Line search failed".to_string()), history } @@ -539,11 +540,11 @@ pub fn realize_gram( // find the kernel of the Hessian. give it the uniform inner product let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); - Ok(Realization { config: state.config, tangent }) + Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) } else { Err("Failed to reach target accuracy".to_string()) }; - RealizationResult{ result, history } + Realization { result, history } } // --- tests --- @@ -562,7 +563,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) -> RealizationResult { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -613,7 +614,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) -> RealizationResult { + pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -737,7 +738,7 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let RealizationResult { result, history } = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); let config = result.unwrap().config; @@ -782,10 +783,10 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let RealizationResult { result, history } = realize_gram( + let Realization { result, history } = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - let Realization { config, tangent } = result.unwrap(); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(config, problem.guess); assert_eq!(history.scaled_loss.len(), 1); @@ -854,8 +855,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let RealizationResult { result, history } = realize_kaleidocycle(SCALED_TOL); - let Realization { config, tangent } = result.unwrap(); + let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -943,10 +944,10 @@ 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 RealizationResult { result: result_orig, history: history_orig } = realize_gram( + let Realization { 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(); + let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); assert_eq!(history_orig.scaled_loss.len(), 1); @@ -964,10 +965,10 @@ mod tests { guess: guess_tfm, frozen: problem_orig.frozen }; - let RealizationResult { result: result_tfm, history: history_tfm } = realize_gram( + let Realization { 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(); + let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); assert_eq!(history_tfm.scaled_loss.len(), 1); -- 2.43.0 From f8e9624fe308171c3c452d583917280910fc9169 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 20 Jul 2025 19:06:26 -0700 Subject: [PATCH 15/16] Make `run-examples.sh` less location-dependent --- app-proto/run-examples.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app-proto/run-examples.sh b/app-proto/run-examples.sh index 50fafbe..0af2e35 100644 --- a/app-proto/run-examples.sh +++ b/app-proto/run-examples.sh @@ -3,8 +3,14 @@ # Karol Kuczmarski. "Add examples to your Rust libraries" # http://xion.io/post/code/rust-examples.html # +# you should invoke this script by calling `sh` or another interpreter, rather +# than calling `souce`, to ensure that the script can find the manifest file for +# the application prototype -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 +# find the manifest file for the application prototype +MANIFEST="$(dirname -- $0)/Cargo.toml" + +cargo run --manifest-path $MANIFEST --example irisawa-hexlet; echo +cargo run --manifest-path $MANIFEST --example three-spheres; echo +cargo run --manifest-path $MANIFEST --example point-on-sphere; echo +cargo run --manifest-path $MANIFEST --example kaleidocycle \ No newline at end of file -- 2.43.0 From edeb080745efe3190f3c38f2eb05bf2d17bdd1dc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 20 Jul 2025 19:28:53 -0700 Subject: [PATCH 16/16] Reduce repetition in `run-examples.sh` --- app-proto/run-examples.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app-proto/run-examples.sh b/app-proto/run-examples.sh index 0af2e35..861addf 100644 --- a/app-proto/run-examples.sh +++ b/app-proto/run-examples.sh @@ -10,7 +10,11 @@ # find the manifest file for the application prototype MANIFEST="$(dirname -- $0)/Cargo.toml" -cargo run --manifest-path $MANIFEST --example irisawa-hexlet; echo -cargo run --manifest-path $MANIFEST --example three-spheres; echo -cargo run --manifest-path $MANIFEST --example point-on-sphere; echo -cargo run --manifest-path $MANIFEST --example kaleidocycle \ No newline at end of file +# set up the command that runs each example +RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" + +# run the examples +$RUN_EXAMPLE irisawa-hexlet; echo +$RUN_EXAMPLE three-spheres; echo +$RUN_EXAMPLE point-on-sphere; echo +$RUN_EXAMPLE kaleidocycle \ No newline at end of file -- 2.43.0