From f4e5c34fde35e5fe2eaa71d0f9d6fb5a7abb1d3d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 19 May 2025 18:54:45 -0700 Subject: [PATCH 1/4] Add a trait method to normalize representations This corrects the dispatching of the normalization routine for spheres, and it adds a (perhaps redundant) normalization routine for points. --- app-proto/src/assembly.rs | 30 ++++++++++++++++-------------- app-proto/src/engine.rs | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e48b802..0aa6dab 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,6 +1,5 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ - any::{Any, TypeId}, cell::Cell, collections::{BTreeMap, BTreeSet}, cmp::Ordering, @@ -19,6 +18,8 @@ use crate::{ Q, change_half_curvature, local_unif_to_std, + normalize_mut_point, + normalize_mut_sphere, point, realize_gram, sphere, @@ -107,6 +108,9 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; + // normalize a representation vector for this kind of element + fn normalize_mut_rep(&self, rep: &mut DVector); + // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never // been through a realization @@ -221,6 +225,10 @@ impl Element for Sphere { self.regulators } + fn normalize_mut_rep(&self, rep: &mut DVector) { + normalize_mut_sphere(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -313,6 +321,10 @@ impl Element for Point { self.regulators } + fn normalize_mut_rep(&self, rep: &mut DVector) { + normalize_mut_point(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -782,24 +794,14 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward - /* KLUDGE */ - // for now, we only restore the normalizations of spheres for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { - // step the assembly along the deformation + // step the element along the deformation and then + // restore its normalization *rep += motion_proj.column(column_index); - - if elt.type_id() == TypeId::of::() { - // restore normalization by contracting toward the - // last coordinate axis - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); - } + elt.normalize_mut_rep(rep); }, None => { console::log_1(&JsValue::from( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index b0fa23d..7bc2bdc 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -34,6 +34,21 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// normalize a sphere's representation vector by contracting toward the last +// coordinate axis +pub fn normalize_mut_sphere(rep: &mut DVector) { + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); +} + +// normalize a point's representation vector by scaling +pub fn normalize_mut_point(rep: &mut DVector) { + rep.scale_mut(0.5 / rep[3]); +} + // given a sphere's representation vector, change the sphere's half-curvature to // `half-curv` and then restore normalization by contracting the representation // vector toward the curvature axis From 0cfdd59e237921355b9e72526c782d72d61ad504 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 20 May 2025 17:26:23 -0700 Subject: [PATCH 2/4] Test curvature drift during position nudging The test fails, as expected, if you disable sphere normalization by making `Sphere::normalize_mut_rep` do nothing. Since the tests aren't built for WebAssembly, we have to replace `console::log` with `console_log!` in all of the functions they use. We'll eventually want to do this replacement everywhere. --- app-proto/Cargo.toml | 3 ++ app-proto/src/assembly.rs | 92 +++++++++++++++++++++++++++------------ app-proto/src/engine.rs | 19 ++++---- 3 files changed, 76 insertions(+), 38 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 844a0a6..5d97193 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,9 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] } + [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0aa6dab..2a23b4a 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -623,9 +623,7 @@ impl Assembly { create_effect(move || { /* DEBUG */ // log the regulator update - console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator.subjects()) - )); + console_log!("Updated regulator with subjects {:?}", regulator.subjects()); if regulator.try_activate() { self_for_effect.realize(); @@ -634,10 +632,10 @@ impl Assembly { /* DEBUG */ // print an updated list of regulators - console::log_1(&JsValue::from("Regulators:")); + console_log!("Regulators:"); self.regulators.with_untracked(|regs| { for reg in regs.into_iter() { - console::log_1(&JsValue::from(format!( + console_log!( " {:?}: {}", reg.subjects(), reg.set_point().with_untracked( @@ -650,7 +648,7 @@ impl Assembly { } } ) - ))); + ); } }); } @@ -681,19 +679,11 @@ impl Assembly { /* DEBUG */ // log the Gram matrix - console::log_1(&JsValue::from("Gram matrix:")); - problem.gram.log_to_console(); + console_log!("Gram matrix:\n{}", problem.gram); /* DEBUG */ // log the initial configuration matrix - console::log_1(&JsValue::from("Old configuration:")); - for j in 0..problem.guess.nrows() { - let mut row_str = String::new(); - for k in 0..problem.guess.ncols() { - row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); - } - console::log_1(&JsValue::from(row_str)); - } + console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( @@ -702,16 +692,14 @@ impl Assembly { /* DEBUG */ // report the outcome of the search - console::log_1(&JsValue::from( - if success { - "Target accuracy achieved!" - } else { - "Failed to reach target accuracy" - } - )); - console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); - console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); - console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim())); + if success { + console_log!("Target accuracy achieved!") + } else { + console_log!("Failed to reach target accuracy") + } + console_log!("Steps: {}", history.scaled_loss.len() - 1); + console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); + console_log!("Tangent dimension: {}", tangent.dim()); if success { // read out the solution @@ -804,9 +792,7 @@ impl Assembly { elt.normalize_mut_rep(rep); }, None => { - console::log_1(&JsValue::from( - format!("No velocity to unpack for fresh element \"{}\"", elt.id()) - )) + console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) } }; }); @@ -823,6 +809,8 @@ impl Assembly { mod tests { use super::*; + use crate::engine; + #[test] #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { @@ -848,4 +836,50 @@ mod tests { }.pose(&mut ConstraintProblem::new(2)); }); } + + #[test] + fn curvature_drift_test() { + const INITIAL_RADIUS: f64 = 0.25; + let _ = create_root(|| { + // set up an assembly containing a single sphere centered at the + // origin + let assembly = Assembly::new(); + let sphere_id = "sphere0"; + let _ = assembly.try_insert_element( + // we create the sphere by hand for two reasons: to choose the + // curvature (which can affect drift rate) and to make the test + // independent of `Sphere::default` + Sphere::new( + String::from(sphere_id), + String::from("Sphere 0"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS) + ) + ); + + // nudge the sphere repeatedly along the `z` axis + const STEP_SIZE: f64 = 0.0025; + const STEP_CNT: usize = 400; + let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); + let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); + for _ in 0..STEP_CNT { + assembly.deform( + vec![ + ElementMotion { + element: sphere.clone(), + velocity: velocity.as_view() + } + ] + ); + } + + // check how much the sphere's curvature has drifted + const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; + const DRIFT_TOL: f64 = 0.015; + let final_half_curv = sphere.representation().with_untracked( + |rep| rep[Sphere::CURVATURE_COMPONENT] + ); + assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); + }); + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 7bc2bdc..03d13a9 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,5 +1,6 @@ use lazy_static::lazy_static; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; +use std::fmt::{Display, Error, Formatter}; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -109,15 +110,6 @@ impl PartialMatrix { } } - /* DEBUG */ - pub fn log_to_console(&self) { - for &MatrixEntry { index: (row, col), value } in self { - console::log_1(&JsValue::from( - format!(" {} {} {}", row, col, value) - )); - } - } - fn freeze(&self, a: &DMatrix) -> DMatrix { let mut result = a.clone(); for &MatrixEntry { index, value } in self { @@ -143,6 +135,15 @@ impl PartialMatrix { } } +impl Display for PartialMatrix { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + for &MatrixEntry { index: (row, col), value } in self { + writeln!(f, " {row} {col} {value}")?; + } + Ok(()) + } +} + impl IntoIterator for PartialMatrix { type Item = MatrixEntry; type IntoIter = std::vec::IntoIter; From e19792d961b4e2fa41d083251f6814ca61cd0e61 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 29 May 2025 17:52:55 -0700 Subject: [PATCH 3/4] Explain the new `check-cfg` lint --- app-proto/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 5d97193..9b46b2b 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,10 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" +# turn off spurious warnings about the custom config that Sycamore uses +# +# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr +# [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] } From f332f755e0966ef8e643e82d13860d3cc18fac86 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 1 Jun 2025 10:49:50 -0700 Subject: [PATCH 4/4] Improve the naming of the normalization methods --- app-proto/src/assembly.rs | 19 ++++++++++--------- app-proto/src/engine.rs | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 2a23b4a..6c91fc0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -18,9 +18,9 @@ use crate::{ Q, change_half_curvature, local_unif_to_std, - normalize_mut_point, - normalize_mut_sphere, point, + project_point_to_normalized, + project_sphere_to_normalized, realize_gram, sphere, ConfigSubspace, @@ -108,8 +108,9 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; - // normalize a representation vector for this kind of element - fn normalize_mut_rep(&self, rep: &mut DVector); + // project a representation vector for this kind of element onto its + // normalization variety + fn project_to_normalized(&self, rep: &mut DVector); // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never @@ -225,8 +226,8 @@ impl Element for Sphere { self.regulators } - fn normalize_mut_rep(&self, rep: &mut DVector) { - normalize_mut_sphere(rep); + fn project_to_normalized(&self, rep: &mut DVector) { + project_sphere_to_normalized(rep); } fn column_index(&self) -> Option { @@ -321,8 +322,8 @@ impl Element for Point { self.regulators } - fn normalize_mut_rep(&self, rep: &mut DVector) { - normalize_mut_point(rep); + fn project_to_normalized(&self, rep: &mut DVector) { + project_point_to_normalized(rep); } fn column_index(&self) -> Option { @@ -789,7 +790,7 @@ impl Assembly { // step the element along the deformation and then // restore its normalization *rep += motion_proj.column(column_index); - elt.normalize_mut_rep(rep); + elt.project_to_normalized(rep); }, None => { console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 03d13a9..c5d7b00 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -35,9 +35,9 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } -// normalize a sphere's representation vector by contracting toward the last -// coordinate axis -pub fn normalize_mut_sphere(rep: &mut DVector) { +// project a sphere's representation vector to the normalization variety by +// contracting toward the last coordinate axis +pub fn project_sphere_to_normalized(rep: &mut DVector) { let q_sp = rep.fixed_rows::<3>(0).norm_squared(); let half_q_lt = -2.0 * rep[3] * rep[4]; let half_q_lt_sq = half_q_lt * half_q_lt; @@ -46,7 +46,7 @@ pub fn normalize_mut_sphere(rep: &mut DVector) { } // normalize a point's representation vector by scaling -pub fn normalize_mut_point(rep: &mut DVector) { +pub fn project_point_to_normalized(rep: &mut DVector) { rep.scale_mut(0.5 / rep[3]); }