From bb226c5f455882ed2293a83740e500435088522d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 24 Mar 2025 23:21:55 -0400 Subject: [PATCH 01/21] Encapsulate the constraint problem data This will make it easier for elements and regulators to write themselves into the constraint problem. --- app-proto/examples/point-on-sphere.rs | 25 +-- app-proto/examples/three-spheres.rs | 29 ++- app-proto/src/assembly.rs | 36 ++-- app-proto/src/engine.rs | 249 ++++++++++++++------------ 4 files changed, 172 insertions(+), 167 deletions(-) diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 13040e5..2820793 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,26 +1,19 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), sphere(0.0, 0.0, 0.0, 1.0) ]); - let frozen = [(3, 0)]; + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + problem.frozen.push((3, 0)); println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 19acfd1..3f3cc44 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,29 +1,22 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = { + let mut problem = ConstraintProblem::from_guess({ let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ + &[ sphere(1.0, 0.0, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0), sphere(-0.5, -a, 0.0, 1.0) - ]) - }; + ] + }); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); if success { diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 18176df..289b271 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -6,7 +6,13 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + engine::{ + Q, + local_unif_to_std, + realize_gram, + ConfigSubspace, + ConstraintProblem + }, specified::SpecifiedValue }; @@ -268,6 +274,11 @@ impl Assembly { // --- realization --- pub fn realize(&self) { + // create a blank constraint problem + let mut problem = ConstraintProblem::new( + self.elements.with_untracked(|elts| elts.len()) + ); + // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { @@ -276,9 +287,8 @@ impl Assembly { }); // set up the Gram matrix and the initial configuration matrix - let (gram, guess) = self.elements.with_untracked(|elts| { + self.elements.with_untracked(|elts| { // set up the off-diagonal part of the Gram matrix - let mut gram_to_be = PartialMatrix::new(); self.regulators.with_untracked(|regs| { for (_, reg) in regs { reg.set_point.with_untracked(|set_pt| { @@ -286,7 +296,7 @@ impl Assembly { let subjects = reg.subjects; let row = elts[subjects.0].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, val); + problem.gram.push_sym(row, col, val); } }); } @@ -294,36 +304,32 @@ impl Assembly { // set up the initial configuration matrix and the diagonal of the // Gram matrix - let mut guess_to_be = DMatrix::::zeros(5, elts.len()); for (_, elt) in elts { let index = elt.column_index.unwrap(); - gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); + problem.gram.push_sym(index, index, 1.0); + problem.guess.set_column(index, &elt.representation.get_clone_untracked()); } - - (gram_to_be, guess_to_be) }); /* DEBUG */ // log the Gram matrix console::log_1(&JsValue::from("Gram matrix:")); - gram.log_to_console(); + problem.gram.log_to_console(); /* DEBUG */ // log the initial configuration matrix console::log_1(&JsValue::from("Old configuration:")); - for j in 0..guess.nrows() { + for j in 0..problem.guess.nrows() { let mut row_str = String::new(); - for k in 0..guess.ncols() { - row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + 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)); } // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 35f898c..deb88bd 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -195,6 +195,34 @@ impl DescentHistory { } } +// --- constraint problems --- + +pub struct ConstraintProblem { + pub gram: PartialMatrix, + pub guess: DMatrix, + pub frozen: Vec<(usize, usize)> +} + +impl ConstraintProblem { + pub fn new(element_count: usize) -> ConstraintProblem { + const ELEMENT_DIM: usize = 5; + ConstraintProblem { + gram: PartialMatrix::new(), + guess: DMatrix::::zeros(ELEMENT_DIM, element_count), + frozen: Vec::new() + } + } + + #[cfg(feature = "dev")] + pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { + ConstraintProblem { + gram: PartialMatrix::new(), + guess: DMatrix::from_columns(guess_columns), + frozen: Vec::new() + } + } +} + // --- gram matrix realization --- // the Lorentz form @@ -289,9 +317,7 @@ fn seek_better_config( // seek a matrix `config` for which `config' * Q * config` matches the partial // matrix `gram`. use gradient descent starting from `guess` pub fn realize_gram( - gram: &PartialMatrix, - guess: DMatrix, - frozen: &[(usize, usize)], + problem: &ConstraintProblem, scaled_tol: f64, min_efficiency: f64, backoff: f64, @@ -299,6 +325,11 @@ pub fn realize_gram( max_descent_steps: i32, max_backoff_steps: i32 ) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + // destructure the problem data + let ConstraintProblem { + gram, guess, frozen + } = problem; + // start the descent history let mut history = DescentHistory::new(); @@ -317,7 +348,7 @@ pub fn realize_gram( ).collect(); // use Newton's method with backtracking and gradient descent backup - let mut state = SearchState::from_config(gram, guess); + let mut state = SearchState::from_config(gram, guess.clone()); let mut hess = DMatrix::zeros(element_dim, assembly_dim); for _ in 0..max_descent_steps { // find the negative gradient of the loss function @@ -415,7 +446,7 @@ pub fn realize_gram( #[cfg(feature = "dev")] pub mod examples { - use std::{array, f64::consts::PI}; + use std::f64::consts::PI; use super::*; @@ -428,35 +459,7 @@ pub mod examples { // https://www.nippon.com/en/japan-topics/c12801/ // pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for s in 0..9 { - // each sphere is represented by a spacelike vector - gram_to_be.push_sym(s, s, 1.0); - - // the circumscribing sphere is tangent to all of the other - // spheres, with matching orientation - if s > 0 { - gram_to_be.push_sym(0, s, 1.0); - } - - if s > 2 { - // each chain sphere is tangent to the "sun" and "moon" - // spheres, with opposing orientation - for n in 1..3 { - gram_to_be.push_sym(s, n, -1.0); - } - - // each chain sphere is tangent to the next chain sphere, - // with opposing orientation - let s_next = 3 + (s-2) % 6; - gram_to_be.push_sym(s, s_next, -1.0); - } - } - gram_to_be - }; - - let guess = DMatrix::from_columns( + let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, -9.0, 5.0), @@ -471,42 +474,45 @@ pub mod examples { ).collect::>().as_slice() ); + for s in 0..9 { + // each sphere is represented by a spacelike vector + problem.gram.push_sym(s, s, 1.0); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + problem.gram.push_sym(0, s, 1.0); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + problem.gram.push_sym(s, n, -1.0); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + problem.gram.push_sym(s, s_next, -1.0); + } + } + // the frozen entries fix the radii of the circumscribing sphere, the // "sun" and "moon" spheres, and one of the chain spheres - let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + for k in 0..4 { + problem.frozen.push((3, k)) + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } // 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) { - const N_POINTS: usize = 12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - - let guess = { - const N_HINGES: usize = 6; - let guess_elts = (0..N_HINGES).step_by(2).flat_map( + const N_HINGES: usize = 6; + let mut problem = ConstraintProblem::from_guess( + (0..N_HINGES).step_by(2).flat_map( |n| { let ang_hor = (n as f64) * PI/3.0; let ang_vert = ((n + 1) as f64) * PI/3.0; @@ -519,16 +525,30 @@ pub mod examples { point(x_vert, y_vert, 0.5) ] } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; + ).collect::>().as_slice() + ); - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + const N_POINTS: usize = 2 * N_HINGES; + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + problem.gram.push_sym(block + j, block_next + k, -0.625); + } + } + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + for k in 0..N_POINTS { + problem.frozen.push((3, k)) + } + + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } } @@ -588,33 +608,29 @@ mod tests { // and the realized configuration should match the initial guess #[test] fn frozen_entry_test() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), sphere(0.0, 0.0, 0.0, 1.0) ]); - let frozen = [(3, 0), (3, 1)]; - println!(); + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + for k in 0..2 { + problem.frozen.push((3, k)); + } let (config, _, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); assert_eq!(success, true); for base_step in history.base_step.into_iter() { - for index in frozen { + for &index in &problem.frozen { assert_eq!(base_step[index], 0.0); } } - for index in frozen { - assert_eq!(config[index], guess[index]); + for index in problem.frozen { + assert_eq!(config[index], problem.guess[index]); } } @@ -635,34 +651,32 @@ mod tests { #[test] fn tangent_test_three_spheres() { const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + const ELEMENT_DIM: usize = 5; + let mut problem = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.0, -2.0), sphere(0.0, 0.0, 1.0, 1.0), sphere(0.0, 0.0, -1.0, 1.0) ]); - let frozen: [_; 5] = std::array::from_fn(|k| (k, 0)); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + for n in 0..ELEMENT_DIM { + problem.frozen.push((n, 0)); + } let (config, tangent, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config, guess); + 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 // the solution variety const UNIFORM_DIM: usize = 4; - let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); + let element_dim = problem.guess.nrows(); + let assembly_dim = problem.guess.ncols(); let tangent_motions_unif = vec![ basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), @@ -805,22 +819,17 @@ mod tests { fn proj_equivar_test() { // find a pair of spheres that meet at 120° const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - gram_to_be.push_sym(0, 0, 1.0); - gram_to_be.push_sym(1, 1, 1.0); - gram_to_be.push_sym(0, 1, 0.5); - gram_to_be - }; - let guess_orig = DMatrix::from_columns(&[ + let mut problem_orig = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.5, 1.0), sphere(0.0, 0.0, -0.5, 1.0) ]); + 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( - &gram, guess_orig.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_orig, guess_orig); + assert_eq!(config_orig, problem_orig.guess); assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); @@ -833,11 +842,15 @@ mod tests { sphere(-a, 0.0, 7.0 - a, 1.0) ]) }; + let problem_tfm = ConstraintProblem { + gram: problem_orig.gram, + guess: guess_tfm, + frozen: problem_orig.frozen + }; let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( - &gram, guess_tfm.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_tfm, guess_tfm); + assert_eq!(config_tfm, problem_tfm.guess); assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); @@ -869,7 +882,7 @@ mod tests { // the comparison tolerance because the transformation seems to // introduce some numerical error const SCALED_TOL_TFM: f64 = 1.0e-9; - let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; + let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); } } \ No newline at end of file -- 2.43.0 From 7c40d60103a3de17ce7fe75d25144421db3367e5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Mar 2025 02:15:03 -0700 Subject: [PATCH 02/21] Let the elements and regulators write the problem When we realize an assembly, each element and regulator now writes its own data into the constraint problem. --- app-proto/src/assembly.rs | 122 +++++++++++++++++++++++++++++--------- app-proto/src/outline.rs | 4 +- 2 files changed, 95 insertions(+), 31 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 289b271..293bf73 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -121,15 +121,43 @@ impl Element { None } } + + fn write_to_problem(&self, problem: &mut ConstraintProblem) { + if let Some(index) = self.column_index { + problem.gram.push_sym(index, index, 1.0); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); + } else { + panic!("Tried to write problem data from an unindexed element: \"{}\"", self.id); + } + } } #[derive(Clone, Copy)] -pub struct Regulator { +pub struct ProductRegulator { pub subjects: (ElementKey, ElementKey), pub measurement: ReadSignal, pub set_point: Signal } +impl ProductRegulator { + fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let subjects = self.subjects; + let subject_column_indices = ( + elts[subjects.0].column_index, + elts[subjects.1].column_index + ); + if let (Some(row), Some(col)) = subject_column_indices { + problem.gram.push_sym(row, col, val); + } else { + panic!("Tried to write problem data from a regulator with an unindexed subject"); + } + } + }); + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, @@ -143,7 +171,7 @@ type AssemblyMotion<'a> = Vec>; pub struct Assembly { // elements and regulators pub elements: Signal>, - pub regulators: Signal>, + pub regulators: Signal>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -214,7 +242,7 @@ impl Assembly { ); } - fn insert_regulator(&self, regulator: Regulator) { + fn insert_regulator(&self, regulator: ProductRegulator) { let subjects = regulator.subjects; let key = self.regulators.update(|regs| regs.insert(regulator)); let subject_regulators = self.elements.with( @@ -236,7 +264,7 @@ impl Assembly { } ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(Regulator { + self.insert_regulator(ProductRegulator { subjects: subjects, measurement: measurement, set_point: set_point @@ -263,7 +291,7 @@ impl Assembly { // edited while acting as a constraint create_effect(move || { console::log_1(&JsValue::from( - format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) + format!("Updated regulator with subjects {:?}", subjects) )); if set_point.with(|set_pt| set_pt.is_present()) { self.realize(); @@ -274,11 +302,6 @@ impl Assembly { // --- realization --- pub fn realize(&self) { - // create a blank constraint problem - let mut problem = ConstraintProblem::new( - self.elements.with_untracked(|elts| elts.len()) - ); - // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { @@ -286,29 +309,18 @@ impl Assembly { } }); - // set up the Gram matrix and the initial configuration matrix - self.elements.with_untracked(|elts| { - // set up the off-diagonal part of the Gram matrix + // set up the constraint problem + let problem = self.elements.with_untracked(|elts| { + let mut problem_to_be = ConstraintProblem::new(elts.len()); + for (_, elt) in elts { + elt.write_to_problem(&mut problem_to_be); + } self.regulators.with_untracked(|regs| { for (_, reg) in regs { - reg.set_point.with_untracked(|set_pt| { - if let Some(val) = set_pt.value { - let subjects = reg.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - problem.gram.push_sym(row, col, val); - } - }); + reg.write_to_problem(&mut problem_to_be, elts); } }); - - // set up the initial configuration matrix and the diagonal of the - // Gram matrix - for (_, elt) in elts { - let index = elt.column_index.unwrap(); - problem.gram.push_sym(index, index, 1.0); - problem.guess.set_column(index, &elt.representation.get_clone_untracked()); - } + problem_to_be }); /* DEBUG */ @@ -464,4 +476,56 @@ impl Assembly { // sync self.realize(); } +} + +#[cfg(test)] +mod tests { + use crate::engine; + + use super::*; + + #[test] + #[should_panic(expected = "Tried to write problem data from an unindexed element: \"sphere\"")] + fn unindexed_element_test() { + let _ = create_root(|| { + Element::new( + "sphere".to_string(), + "Sphere".to_string(), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ).write_to_problem(&mut ConstraintProblem::new(1)); + }); + } + + #[test] + #[should_panic(expected = "Tried to write problem data from a regulator with an unindexed subject")] + fn unindexed_subject_test() { + let _ = create_root(|| { + let mut elts = Slab::new(); + let subjects = ( + elts.insert( + Element::new( + "sphere0".to_string(), + "Sphere 0".to_string(), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ), + elts.insert( + Element::new( + "sphere1".to_string(), + "Sphere 1".to_string(), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ) + ); + elts[subjects.0].column_index = Some(0); + ProductRegulator { + subjects: subjects, + measurement: create_memo(|| 0.0), + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) + }.write_to_problem(&mut ConstraintProblem::new(2), &elts); + }); + } } \ No newline at end of file diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 002baea..deede23 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -9,13 +9,13 @@ use web_sys::{ use crate::{ AppState, assembly, - assembly::{ElementKey, Regulator, RegulatorKey}, + assembly::{ElementKey, ProductRegulator, RegulatorKey}, specified::SpecifiedValue }; // an editable view of a regulator #[component(inline_props)] -fn RegulatorInput(regulator: Regulator) -> View { +fn RegulatorInput(regulator: ProductRegulator) -> View { let valid = create_signal(true); let value = create_signal( regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) -- 2.43.0 From 00f60b0e903b561fc1931b3948e2d18bd147d1bb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Mar 2025 23:50:40 -0700 Subject: [PATCH 03/21] Store a product regulator's subjects in an array This lets us iterate over subjects. Based on commit 257ce33, with a few updates from 4a9e777. --- app-proto/src/add_remove.rs | 15 +++++++---- app-proto/src/assembly.rs | 52 ++++++++++++++++--------------------- app-proto/src/outline.rs | 6 ++--- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 5fed411..cd8b4f9 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -188,11 +188,16 @@ pub fn AddRemove() -> View { }, on:click=|_| { let state = use_context::(); - let subjects = state.selection.with( - |sel| { - let subject_vec: Vec<_> = sel.into_iter().collect(); - (subject_vec[0].clone(), subject_vec[1].clone()) - } + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() ); state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 293bf73..d006b7b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -134,7 +134,7 @@ impl Element { #[derive(Clone, Copy)] pub struct ProductRegulator { - pub subjects: (ElementKey, ElementKey), + pub subjects: [ElementKey; 2], pub measurement: ReadSignal, pub set_point: Signal } @@ -143,12 +143,10 @@ impl ProductRegulator { fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let subjects = self.subjects; - let subject_column_indices = ( - elts[subjects.0].column_index, - elts[subjects.1].column_index + let subject_column_indices = self.subjects.map( + |subj| elts[subj].column_index ); - if let (Some(row), Some(col)) = subject_column_indices { + if let [Some(row), Some(col)] = subject_column_indices { problem.gram.push_sym(row, col, val); } else { panic!("Tried to write problem data from a regulator with an unindexed subject"); @@ -246,21 +244,23 @@ impl Assembly { let subjects = regulator.subjects; let key = self.regulators.update(|regs| regs.insert(regulator)); let subject_regulators = self.elements.with( - |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) + |elts| subjects.map(|subj| elts[subj].regulators) ); - subject_regulators.0.update(|regs| regs.insert(key)); - subject_regulators.1.update(|regs| regs.insert(key)); + for regulators in subject_regulators { + regulators.update(|regs| regs.insert(key)); + } } - pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { + pub fn insert_new_regulator(self, subjects: [ElementKey; 2]) { // create and insert a new regulator let measurement = self.elements.map( move |elts| { - let reps = ( - elts[subjects.0].representation.get_clone(), - elts[subjects.1].representation.get_clone() - ); - reps.0.dot(&(&*Q * reps.1)) + let representations = subjects.map(|subj| elts[subj].representation); + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) + ) + ) } ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); @@ -277,8 +277,8 @@ impl Assembly { for (_, reg) in regs.into_iter() { console::log_5( &JsValue::from(" "), - &JsValue::from(reg.subjects.0), - &JsValue::from(reg.subjects.1), + &JsValue::from(reg.subjects[0]), + &JsValue::from(reg.subjects[1]), &JsValue::from(":"), ®.set_point.with_untracked( |set_pt| JsValue::from(set_pt.spec.as_str()) @@ -502,25 +502,17 @@ mod tests { fn unindexed_subject_test() { let _ = create_root(|| { let mut elts = Slab::new(); - let subjects = ( + let subjects = [0, 1].map(|k| { elts.insert( Element::new( - "sphere0".to_string(), - "Sphere 0".to_string(), - [1.0_f32, 1.0_f32, 1.0_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) - ), - elts.insert( - Element::new( - "sphere1".to_string(), - "Sphere 1".to_string(), + "sphere{k}".to_string(), + "Sphere {k}".to_string(), [1.0_f32, 1.0_f32, 1.0_f32], engine::sphere(0.0, 0.0, 0.0, 1.0) ) ) - ); - elts[subjects.0].column_index = Some(0); + }); + elts[subjects[0]].column_index = Some(0); ProductRegulator { subjects: subjects, measurement: create_memo(|| 0.0), diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index deede23..2951e69 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -81,10 +81,10 @@ fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> let state = use_context::(); let assembly = &state.assembly; let regulator = assembly.regulators.with(|regs| regs[regulator_key]); - let other_subject = if regulator.subjects.0 == element_key { - regulator.subjects.1 + let other_subject = if regulator.subjects[0] == element_key { + regulator.subjects[1] } else { - regulator.subjects.0 + regulator.subjects[0] }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); view! { -- 2.43.0 From 126d4c0cce14c33fd1a2ed1e249b4b78af5a64c2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 27 Mar 2025 00:29:27 -0700 Subject: [PATCH 04/21] Introduce a regulator trait This will provide a common interface for Lorentz product regulators, curvature regulators, and hopefully all the other regulators too. --- app-proto/src/add_remove.rs | 2 +- app-proto/src/assembly.rs | 72 ++++++++++++++++++++++++++----------- app-proto/src/outline.rs | 36 ++++++++++++------- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index cd8b4f9..8bf6bb1 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -199,7 +199,7 @@ pub fn AddRemove() -> View { .try_into() .unwrap() ); - state.assembly.insert_new_regulator(subjects); + state.assembly.insert_new_product_regulator(subjects); state.selection.update(|sel| sel.clear()); } ) { "🔗" } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index d006b7b..fe08c91 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,7 +1,7 @@ use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; +use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -132,14 +132,35 @@ impl Element { } } -#[derive(Clone, Copy)] +pub trait Regulator { + // get information + fn subjects(&self) -> Vec; + fn measurement(&self) -> ReadSignal; + fn set_point(&self) -> Signal; + + // write problem data + fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab); +} + pub struct ProductRegulator { pub subjects: [ElementKey; 2], pub measurement: ReadSignal, pub set_point: Signal } -impl ProductRegulator { +impl Regulator for ProductRegulator { + fn subjects(&self) -> Vec { + self.subjects.into() + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } + fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { @@ -169,7 +190,7 @@ type AssemblyMotion<'a> = Vec>; pub struct Assembly { // elements and regulators pub elements: Signal>, - pub regulators: Signal>, + pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -240,19 +261,23 @@ impl Assembly { ); } - fn insert_regulator(&self, regulator: ProductRegulator) { - let subjects = regulator.subjects; - let key = self.regulators.update(|regs| regs.insert(regulator)); - let subject_regulators = self.elements.with( - |elts| subjects.map(|subj| elts[subj].regulators) + fn insert_regulator(&self, regulator: Rc) { + let subjects = regulator.subjects(); + let key = self.regulators.update( + |regs| regs.insert(regulator) + ); + let subject_regulators: Vec<_> = self.elements.with( + |elts| subjects.into_iter().map( + |subj| elts[subj].regulators + ).collect() ); for regulators in subject_regulators { regulators.update(|regs| regs.insert(key)); } } - pub fn insert_new_regulator(self, subjects: [ElementKey; 2]) { - // create and insert a new regulator + pub fn insert_new_product_regulator(self, subjects: [ElementKey; 2]) { + // create and insert a new product regulator let measurement = self.elements.map( move |elts| { let representations = subjects.map(|subj| elts[subj].representation); @@ -264,26 +289,31 @@ impl Assembly { } ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(ProductRegulator { + self.insert_regulator(Rc::new(ProductRegulator { subjects: subjects, measurement: measurement, set_point: set_point - }); + })); /* DEBUG */ // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); self.regulators.with(|regs| { for (_, reg) in regs.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(reg.subjects[0]), - &JsValue::from(reg.subjects[1]), - &JsValue::from(":"), - ®.set_point.with_untracked( - |set_pt| JsValue::from(set_pt.spec.as_str()) + console::log_1(&JsValue::from(format!( + " {:?}: {}", + reg.subjects(), + reg.set_point().with_untracked( + |set_pt| { + let spec = &set_pt.spec; + if spec.is_empty() { + "__".to_string() + } else { + spec.clone() + } + } ) - ); + ))); } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2951e69..497677d 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use std::rc::Rc; use sycamore::prelude::*; use web_sys::{ KeyboardEvent, @@ -9,24 +10,32 @@ use web_sys::{ use crate::{ AppState, assembly, - assembly::{ElementKey, ProductRegulator, RegulatorKey}, + assembly::{ElementKey, Regulator, RegulatorKey}, specified::SpecifiedValue }; // an editable view of a regulator #[component(inline_props)] -fn RegulatorInput(regulator: ProductRegulator) -> View { +fn RegulatorInput(regulator: Rc) -> View { + // get the regulator's measurement and set point signals + let measurement = regulator.measurement(); + let set_point = regulator.set_point(); + + // the `valid` signal tracks whether the last entered value is a valid set + // point specification let valid = create_signal(true); + + // the `value` signal holds the current set point specification let value = create_signal( - regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) + set_point.with_untracked(|set_pt| set_pt.spec.clone()) ); - // this closure resets the input value to the regulator's set point - // specification + // this `reset_value` closure resets the input value to the regulator's set + // point specification let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); + value.set(set_point.with(|set_pt| set_pt.spec.clone())); }) }; @@ -39,7 +48,7 @@ fn RegulatorInput(regulator: ProductRegulator) -> View { r#type="text", class=move || { if valid.get() { - regulator.set_point.with(|set_pt| { + set_point.with(|set_pt| { if set_pt.is_present() { "regulator-input constraint" } else { @@ -50,13 +59,13 @@ fn RegulatorInput(regulator: ProductRegulator) -> View { "regulator-input invalid" } }, - placeholder=regulator.measurement.with(|result| result.to_string()), + placeholder=measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| { valid.set( match SpecifiedValue::try_from(value.get_clone_untracked()) { Ok(set_pt) => { - regulator.set_point.set(set_pt); + set_point.set(set_pt); true } Err(_) => false @@ -80,11 +89,12 @@ fn RegulatorInput(regulator: ProductRegulator) -> View { fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); let assembly = &state.assembly; - let regulator = assembly.regulators.with(|regs| regs[regulator_key]); - let other_subject = if regulator.subjects[0] == element_key { - regulator.subjects[1] + let regulator = assembly.regulators.with(|regs| regs[regulator_key].clone()); + let subjects = regulator.subjects(); + let other_subject = if subjects[0] == element_key { + subjects[1] } else { - regulator.subjects[0] + subjects[0] }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); view! { -- 2.43.0 From 96e4a34fa1ebbcae1cd21f42bcb64e6452eeb74f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 27 Mar 2025 02:56:46 -0700 Subject: [PATCH 05/21] Interpolate sphere ID and label, as intended Thanks, Clippy! --- app-proto/src/assembly.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index fe08c91..dc0bf91 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -535,8 +535,8 @@ mod tests { let subjects = [0, 1].map(|k| { elts.insert( Element::new( - "sphere{k}".to_string(), - "Sphere {k}".to_string(), + format!("sphere{k}"), + format!("Sphere {k}"), [1.0_f32, 1.0_f32, 1.0_f32], engine::sphere(0.0, 0.0, 0.0, 1.0) ) -- 2.43.0 From d57ff59730187568645d14721b9e85c83724ea10 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 27 Mar 2025 13:57:46 -0700 Subject: [PATCH 06/21] Specify the values of the frozen entries Before, a `ConstraintProblem` only specified the indices of the frozen entries. During realization, the frozen entries kept whatever values they had in the initial guess. This commit adds the values of the frozen entries to the `frozen` field of `ConstraintProblem`. The frozen entries of the guess are set to the desired values at the beginning of realization. This commit also improves the `PartialMatrix` structure, which is used to specify the indices and values of the frozen entries. --- app-proto/examples/point-on-sphere.rs | 2 +- app-proto/src/engine.rs | 135 +++++++++++++++++--------- 2 files changed, 91 insertions(+), 46 deletions(-) diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 2820793..880d7b0 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -10,7 +10,7 @@ fn main() { problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); } } - problem.frozen.push((3, 0)); + problem.frozen.push(3, 0, problem.guess[(3, 0)]); println!(); let (config, _, success, history) = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index deb88bd..44f44e0 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -37,7 +37,7 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 // --- partial matrices --- -struct MatrixEntry { +pub struct MatrixEntry { index: (usize, usize), value: f64 } @@ -49,42 +49,72 @@ impl PartialMatrix { PartialMatrix(Vec::::new()) } - pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + pub fn push(&mut self, row: usize, col: usize, value: f64) { let PartialMatrix(entries) = self; entries.push(MatrixEntry { index: (row, col), value: value }); + } + + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + self.push(row, col, value); if row != col { - entries.push(MatrixEntry { index: (col, row), value: value }); + self.push(col, row, value); } } /* DEBUG */ pub fn log_to_console(&self) { - let PartialMatrix(entries) = self; - for ent in entries { - let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); - console::log_1(&JsValue::from(ent_str.as_str())); + 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 { + result[index] = value; + } + result + } + fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = a[ent.index]; + for &MatrixEntry { index, .. } in self { + result[index] = a[index]; } result } fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = ent.value - rhs[ent.index]; + for &MatrixEntry { index, value } in self { + result[index] = value - rhs[index]; } result } } +impl IntoIterator for PartialMatrix { + type Item = MatrixEntry; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + +impl<'a> IntoIterator for &'a PartialMatrix { + type Item = &'a MatrixEntry; + type IntoIter = std::slice::Iter<'a, MatrixEntry>; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + // --- configuration subspaces --- #[derive(Clone)] @@ -199,8 +229,8 @@ impl DescentHistory { pub struct ConstraintProblem { pub gram: PartialMatrix, + pub frozen: PartialMatrix, pub guess: DMatrix, - pub frozen: Vec<(usize, usize)> } impl ConstraintProblem { @@ -208,8 +238,8 @@ impl ConstraintProblem { const ELEMENT_DIM: usize = 5; ConstraintProblem { gram: PartialMatrix::new(), - guess: DMatrix::::zeros(ELEMENT_DIM, element_count), - frozen: Vec::new() + frozen: PartialMatrix::new(), + guess: DMatrix::::zeros(ELEMENT_DIM, element_count) } } @@ -217,8 +247,8 @@ impl ConstraintProblem { pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { ConstraintProblem { gram: PartialMatrix::new(), - guess: DMatrix::from_columns(guess_columns), - frozen: Vec::new() + frozen: PartialMatrix::new(), + guess: DMatrix::from_columns(guess_columns) } } } @@ -314,8 +344,10 @@ fn seek_better_config( None } -// seek a matrix `config` for which `config' * Q * config` matches the partial -// matrix `gram`. use gradient descent starting from `guess` +// 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 +// use a regularized Newton's method to seek the desired Gram matrix pub fn realize_gram( problem: &ConstraintProblem, scaled_tol: f64, @@ -344,11 +376,11 @@ pub fn realize_gram( // convert the frozen indices to stacked format let frozen_stacked: Vec = frozen.into_iter().map( - |index| index.1*element_dim + index.0 + |MatrixEntry { index: (row, col), .. }| col*element_dim + row ).collect(); - // use Newton's method with backtracking and gradient descent backup - let mut state = SearchState::from_config(gram, guess.clone()); + // use a regularized Newton's method with backtracking + let mut state = SearchState::from_config(gram, frozen.freeze(guess)); let mut hess = DMatrix::zeros(element_dim, assembly_dim); for _ in 0..max_descent_steps { // find the negative gradient of the loss function @@ -501,7 +533,7 @@ pub mod examples { // the frozen entries fix the radii of the circumscribing sphere, the // "sun" and "moon" spheres, and one of the chain spheres for k in 0..4 { - problem.frozen.push((3, k)) + problem.frozen.push(3, k, problem.guess[(3, k)]); } realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) @@ -545,7 +577,7 @@ pub mod examples { } for k in 0..N_POINTS { - problem.frozen.push((3, k)) + problem.frozen.push(3, k, problem.guess[(3, k)]) } realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) @@ -559,6 +591,25 @@ mod tests { use super::{*, examples::*}; + #[test] + fn freeze_test() { + let frozen = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), value: 14.0 }, + MatrixEntry { index: (0, 2), value: 28.0 }, + MatrixEntry { index: (1, 1), value: 42.0 }, + MatrixEntry { index: (1, 2), value: 49.0 } + ]); + let config = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 14.0, 2.0, 28.0, + 4.0, 42.0, 49.0 + ]); + assert_eq!(frozen.freeze(&config), expected_result); + } + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -580,18 +631,12 @@ mod tests { #[test] fn zero_loss_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } + let mut gram = PartialMatrix::new(); + for j in 0..3 { + for k in 0..3 { + gram.push(j, k, if j == k { 1.0 } else { -1.0 }); } - entries - }); + } let config = { let a = 0.75_f64.sqrt(); DMatrix::from_columns(&[ @@ -604,33 +649,33 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + /* TO DO */ // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should match the initial guess + // and the realized configuration should have the desired values #[test] fn frozen_entry_test() { let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) + sphere(0.0, 0.0, 0.0, 0.95) ]); for j in 0..2 { for k in j..2 { problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); } } - for k in 0..2 { - problem.frozen.push((3, k)); - } + problem.frozen.push(3, 0, problem.guess[(3, 0)]); + problem.frozen.push(3, 1, 0.5); let (config, _, success, history) = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); assert_eq!(success, true); for base_step in history.base_step.into_iter() { - for &index in &problem.frozen { + for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); } } - for index in problem.frozen { - assert_eq!(config[index], problem.guess[index]); + for MatrixEntry { index, value } in problem.frozen { + assert_eq!(config[index], value); } } @@ -663,7 +708,7 @@ mod tests { } } for n in 0..ELEMENT_DIM { - problem.frozen.push((n, 0)); + problem.frozen.push(n, 0, problem.guess[(n, 0)]); } let (config, tangent, success, history) = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 -- 2.43.0 From bba0ac3cd60c1b9da27c438034d6e7d9f7c17f7c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 1 Apr 2025 22:23:08 -0700 Subject: [PATCH 07/21] Add a half-curvature regulator In the process, add the `OutlineItem` trait so that each regulator can implement its own outline item view. --- app-proto/src/add_remove.rs | 13 +-- app-proto/src/assembly.rs | 160 +++++++++++++++++++++++++++++------- app-proto/src/outline.rs | 78 +++++++++++++----- 3 files changed, 189 insertions(+), 62 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 8bf6bb1..f44f5c0 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -166,18 +166,7 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_element(); - - /* DEBUG */ - // print updated list of elements by identifier - console::log_1(&JsValue::from("elements by identifier:")); - for (id, key) in state.assembly.elements_by_id.get_clone().iter() { - console::log_3( - &JsValue::from(" "), - &JsValue::from(id), - &JsValue::from(*key) - ); - } + state.assembly.insert_new_sphere(); } ) { "+" } button( diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index dc0bf91..f41397b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -10,9 +10,11 @@ use crate::{ Q, local_unif_to_std, realize_gram, + sphere, ConfigSubspace, ConstraintProblem }, + outline::OutlineItem, specified::SpecifiedValue }; @@ -36,8 +38,8 @@ pub struct Element { pub color: ElementColor, pub representation: Signal>, - // All regulators with this element as a subject. The assembly owning - // this element is responsible for keeping this set up to date. + // the regulators this element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date pub regulators: Signal>, // a serial number, assigned by `Element::new`, that uniquely identifies @@ -132,7 +134,7 @@ impl Element { } } -pub trait Regulator { +pub trait Regulator: OutlineItem { // get information fn subjects(&self) -> Vec; fn measurement(&self) -> ReadSignal; @@ -177,6 +179,39 @@ impl Regulator for ProductRegulator { } } +pub struct HalfCurvatureRegulator { + pub subject: ElementKey, + pub measurement: ReadSignal, + pub set_point: Signal +} + +impl Regulator for HalfCurvatureRegulator { + fn subjects(&self) -> Vec { + vec![self.subject] + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } + + fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + if let Some(col) = elts[self.subject].column_index { + const CURVATURE_COMPONENT: usize = 3; + problem.frozen.push(CURVATURE_COMPONENT, col, val); + } else { + panic!("Tried to write problem data from a regulator with an unindexed subject"); + } + } + }); + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, @@ -223,23 +258,25 @@ impl Assembly { // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: Element) { + fn insert_element_unchecked(&self, elt: Element) -> ElementKey { let id = elt.id.clone(); let key = self.elements.update(|elts| elts.insert(elt)); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + key } - pub fn try_insert_element(&self, elt: Element) -> bool { + pub fn try_insert_element(&self, elt: Element) -> Option { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(&elt.id) ); if can_insert { - self.insert_element_unchecked(elt); + Some(self.insert_element_unchecked(elt)) + } else { + None } - can_insert } - pub fn insert_new_element(&self) { + pub fn insert_new_sphere(self) { // find the next unused identifier in the default sequence let mut id_num = 1; let mut id = format!("sphere{}", id_num); @@ -250,15 +287,18 @@ impl Assembly { id = format!("sphere{}", id_num); } - // create and insert a new element - self.insert_element_unchecked( + // create and insert a sphere + let key = self.insert_element_unchecked( Element::new( id, format!("Sphere {}", id_num), [0.75_f32, 0.75_f32, 0.75_f32], - DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + sphere(0.0, 0.0, 0.0, 1.0) ) ); + + // create and insert a curvature regulator + self.insert_new_half_curvature_regulator(key); } fn insert_regulator(&self, regulator: Rc) { @@ -274,26 +314,6 @@ impl Assembly { for regulators in subject_regulators { regulators.update(|regs| regs.insert(key)); } - } - - pub fn insert_new_product_regulator(self, subjects: [ElementKey; 2]) { - // create and insert a new product regulator - let measurement = self.elements.map( - move |elts| { - let representations = subjects.map(|subj| elts[subj].representation); - representations[0].with(|rep_0| - representations[1].with(|rep_1| - rep_0.dot(&(&*Q * rep_1)) - ) - ) - } - ); - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(Rc::new(ProductRegulator { - subjects: subjects, - measurement: measurement, - set_point: set_point - })); /* DEBUG */ // print an updated list of regulators @@ -316,6 +336,26 @@ impl Assembly { ))); } }); + } + + pub fn insert_new_product_regulator(self, subjects: [ElementKey; 2]) { + // create and insert a new product regulator + let measurement = self.elements.map( + move |elts| { + let representations = subjects.map(|subj| elts[subj].representation); + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) + ) + ) + } + ); + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + self.insert_regulator(Rc::new(ProductRegulator { + subjects: subjects, + measurement: measurement, + set_point: set_point + })); // update the realization when the regulator becomes a constraint, or is // edited while acting as a constraint @@ -329,6 +369,64 @@ impl Assembly { }); } + pub fn insert_new_half_curvature_regulator(self, subject: ElementKey) { + // create and insert a new half-curvature regulator + let measurement = self.elements.map( + move |elts| elts[subject].representation.with(|rep| rep[3]) + ); + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + self.insert_regulator(Rc::new(HalfCurvatureRegulator { + subject: subject, + measurement: measurement, + set_point: set_point + })); + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + create_effect(move || { + console::log_1(&JsValue::from( + format!("Updated regulator with subjects [{}]", subject) + )); + if let Some(half_curv) = set_point.with(|set_pt| set_pt.value) { + let representation = self.elements.with( + |elts| elts[subject].representation + ); + representation.update(|rep| { + // set the sphere's half-curvature to the desired value + rep[3] = half_curv; + + // restore normalization by contracting toward the curvature + // axis + const SIZE_THRESHOLD: f64 = 1e-9; + let half_q_lt = -2.0 * half_curv * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let mut spatial = rep.fixed_rows_mut::<3>(0); + let q_sp = spatial.norm_squared(); + if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD { + spatial.copy_from_slice( + &[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()] + ); + } else { + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + spatial.scale_mut(1.0 / scaling); + rep[4] /= scaling; + } + + /* DEBUG */ + // verify normalization + let rep_for_debug = rep.clone(); + console::log_1(&JsValue::from( + format!( + "Sphere self-product after curvature change: {}", + rep_for_debug.dot(&(&*Q * &rep_for_debug)) + ) + )); + }); + self.realize(); + } + }); + } + // --- realization --- pub fn realize(&self) { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 497677d..32ff2e7 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -10,7 +10,13 @@ use web_sys::{ use crate::{ AppState, assembly, - assembly::{ElementKey, Regulator, RegulatorKey}, + assembly::{ + ElementKey, + HalfCurvatureRegulator, + ProductRegulator, + Regulator, + RegulatorKey + }, specified::SpecifiedValue }; @@ -84,27 +90,53 @@ fn RegulatorInput(regulator: Rc) -> View { } } +pub trait OutlineItem { + fn outline_item(self: Rc, element_key: ElementKey) -> View; +} + +impl OutlineItem for ProductRegulator { + fn outline_item(self: Rc, element_key: ElementKey) -> View { + let state = use_context::(); + let other_subject = if self.subjects[0] == element_key { + self.subjects[1] + } else { + self.subjects[0] + }; + let other_subject_label = state.assembly.elements.with( + |elts| elts[other_subject].label.clone() + ); + view! { + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + +impl OutlineItem for HalfCurvatureRegulator { + fn outline_item(self: Rc, _element_key: ElementKey) -> View { + view! { + li(class="regulator") { + div(class="regulator-label") // for spacing + div(class="regulator-type") { "Half-curvature" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + // a list item that shows a regulator in an outline view of an element #[component(inline_props)] fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); - let assembly = &state.assembly; - let regulator = assembly.regulators.with(|regs| regs[regulator_key].clone()); - let subjects = regulator.subjects(); - let other_subject = if subjects[0] == element_key { - subjects[1] - } else { - subjects[0] - }; - let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - view! { - li(class="regulator") { - div(class="regulator-label") { (other_subject_label) } - div(class="regulator-type") { "Inversive distance" } - RegulatorInput(regulator=regulator) - div(class="status") - } - } + let regulator = state.assembly.regulators.with( + |regs| regs[regulator_key].clone() + ); + regulator.outline_item(element_key) } // a list item that shows an element in an outline view of an assembly @@ -127,7 +159,15 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { }; let regulated = element.regulators.map(|regs| regs.len() > 0); let regulator_list = element.regulators.map( - |regs| regs.clone().into_iter().collect() + move |elt_reg_keys| elt_reg_keys + .clone() + .into_iter() + .sorted_by_key( + |®_key| state.assembly.regulators.with( + |regs| regs[reg_key].subjects().len() + ) + ) + .collect() ); let details_node = create_node_ref(); view! { -- 2.43.0 From 63e3d733ba29c8053cda857ca7cd771cd10400fb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 3 Apr 2025 14:13:45 -0700 Subject: [PATCH 08/21] Introduce a problem poser trait Elements and regulators use this common interface to write their data into the constraint problem. --- app-proto/src/assembly.rs | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index f41397b..33c8c91 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -31,6 +31,10 @@ pub type ElementColor = [f32; 3]; // each assembly has a key that identifies it within the sesssion static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); +pub trait ProblemPoser { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab); +} + #[derive(Clone, PartialEq)] pub struct Element { pub id: String, @@ -123,8 +127,10 @@ impl Element { None } } - - fn write_to_problem(&self, problem: &mut ConstraintProblem) { +} + +impl ProblemPoser for Element { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { if let Some(index) = self.column_index { problem.gram.push_sym(index, index, 1.0); problem.guess.set_column(index, &self.representation.get_clone_untracked()); @@ -134,14 +140,10 @@ impl Element { } } -pub trait Regulator: OutlineItem { - // get information +pub trait Regulator: ProblemPoser + OutlineItem { fn subjects(&self) -> Vec; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; - - // write problem data - fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab); } pub struct ProductRegulator { @@ -162,8 +164,10 @@ impl Regulator for ProductRegulator { fn set_point(&self) -> Signal { self.set_point } - - fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab) { +} + +impl ProblemPoser for ProductRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { let subject_column_indices = self.subjects.map( @@ -197,8 +201,10 @@ impl Regulator for HalfCurvatureRegulator { fn set_point(&self) -> Signal { self.set_point } - - fn write_to_problem(&self, problem: &mut ConstraintProblem, elts: &Slab) { +} + +impl ProblemPoser for HalfCurvatureRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { if let Some(col) = elts[self.subject].column_index { @@ -441,11 +447,11 @@ impl Assembly { let problem = self.elements.with_untracked(|elts| { let mut problem_to_be = ConstraintProblem::new(elts.len()); for (_, elt) in elts { - elt.write_to_problem(&mut problem_to_be); + elt.pose(&mut problem_to_be, elts); } self.regulators.with_untracked(|regs| { for (_, reg) in regs { - reg.write_to_problem(&mut problem_to_be, elts); + reg.pose(&mut problem_to_be, elts); } }); problem_to_be @@ -621,7 +627,7 @@ mod tests { "Sphere".to_string(), [1.0_f32, 1.0_f32, 1.0_f32], engine::sphere(0.0, 0.0, 0.0, 1.0) - ).write_to_problem(&mut ConstraintProblem::new(1)); + ).pose(&mut ConstraintProblem::new(1), &Slab::new()); }); } @@ -645,7 +651,7 @@ mod tests { subjects: subjects, measurement: create_memo(|| 0.0), set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) - }.write_to_problem(&mut ConstraintProblem::new(2), &elts); + }.pose(&mut ConstraintProblem::new(2), &elts); }); } } \ No newline at end of file -- 2.43.0 From 81e423fcbe9ea13c67e720192e63cc7cfc99ecd2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 8 Apr 2025 18:49:48 -0700 Subject: [PATCH 09/21] Give every sphere a curvature regulator In the process, fix a reactivity bug by removing unintended signal tracking from `insert_regulator`. --- app-proto/src/add_remove.rs | 28 ++++++++++++++-------------- app-proto/src/assembly.rs | 36 ++++++++++++++++++++---------------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index f44f5c0..573bd36 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -11,7 +11,7 @@ use crate::{ // load an example assembly for testing. this code will be removed once we've // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_a"), String::from("Castor"), @@ -19,7 +19,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_b"), String::from("Pollux"), @@ -27,7 +27,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_major"), String::from("Ursa major"), @@ -35,7 +35,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_minor"), String::from("Ursa minor"), @@ -43,7 +43,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_deimos"), String::from("Deimos"), @@ -51,7 +51,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_phobos"), String::from("Phobos"), @@ -66,7 +66,7 @@ fn load_gen_assemb(assembly: &Assembly) { // built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "central".to_string(), "Central".to_string(), @@ -74,7 +74,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "assemb_plane".to_string(), "Assembly plane".to_string(), @@ -82,7 +82,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side1".to_string(), "Side 1".to_string(), @@ -90,7 +90,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side2".to_string(), "Side 2".to_string(), @@ -98,7 +98,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side3".to_string(), "Side 3".to_string(), @@ -106,7 +106,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner1".to_string(), "Corner 1".to_string(), @@ -114,7 +114,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner2".to_string(), "Corner 2".to_string(), @@ -122,7 +122,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("corner3"), String::from("Corner 3"), diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 33c8c91..387b567 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -261,28 +261,33 @@ impl Assembly { // --- inserting elements and regulators --- - // insert an element into the assembly without checking whether we already + // insert a sphere into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: Element) -> ElementKey { + fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey { + // insert the sphere let id = elt.id.clone(); let key = self.elements.update(|elts| elts.insert(elt)); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + + // regulate the sphere's curvature + self.insert_new_half_curvature_regulator(key); + key } - pub fn try_insert_element(&self, elt: Element) -> Option { + pub fn try_insert_sphere(&self, elt: Element) -> Option { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(&elt.id) ); if can_insert { - Some(self.insert_element_unchecked(elt)) + Some(self.insert_sphere_unchecked(elt)) } else { None } } - pub fn insert_new_sphere(self) { + pub fn insert_new_sphere(&self) { // find the next unused identifier in the default sequence let mut id_num = 1; let mut id = format!("sphere{}", id_num); @@ -294,7 +299,7 @@ impl Assembly { } // create and insert a sphere - let key = self.insert_element_unchecked( + let _ = self.insert_sphere_unchecked( Element::new( id, format!("Sphere {}", id_num), @@ -302,9 +307,6 @@ impl Assembly { sphere(0.0, 0.0, 0.0, 1.0) ) ); - - // create and insert a curvature regulator - self.insert_new_half_curvature_regulator(key); } fn insert_regulator(&self, regulator: Rc) { @@ -312,7 +314,7 @@ impl Assembly { let key = self.regulators.update( |regs| regs.insert(regulator) ); - let subject_regulators: Vec<_> = self.elements.with( + let subject_regulators: Vec<_> = self.elements.with_untracked( |elts| subjects.into_iter().map( |subj| elts[subj].regulators ).collect() @@ -324,7 +326,7 @@ impl Assembly { /* DEBUG */ // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); - self.regulators.with(|regs| { + self.regulators.with_untracked(|regs| { for (_, reg) in regs.into_iter() { console::log_1(&JsValue::from(format!( " {:?}: {}", @@ -344,7 +346,7 @@ impl Assembly { }); } - pub fn insert_new_product_regulator(self, subjects: [ElementKey; 2]) { + pub fn insert_new_product_regulator(&self, subjects: [ElementKey; 2]) { // create and insert a new product regulator let measurement = self.elements.map( move |elts| { @@ -365,17 +367,18 @@ impl Assembly { // update the realization when the regulator becomes a constraint, or is // edited while acting as a constraint + let self_for_effect = self.clone(); create_effect(move || { console::log_1(&JsValue::from( format!("Updated regulator with subjects {:?}", subjects) )); if set_point.with(|set_pt| set_pt.is_present()) { - self.realize(); + self_for_effect.realize(); } }); } - pub fn insert_new_half_curvature_regulator(self, subject: ElementKey) { + pub fn insert_new_half_curvature_regulator(&self, subject: ElementKey) { // create and insert a new half-curvature regulator let measurement = self.elements.map( move |elts| elts[subject].representation.with(|rep| rep[3]) @@ -389,12 +392,13 @@ impl Assembly { // update the realization when the regulator becomes a constraint, or is // edited while acting as a constraint + let self_for_effect = self.clone(); create_effect(move || { console::log_1(&JsValue::from( format!("Updated regulator with subjects [{}]", subject) )); if let Some(half_curv) = set_point.with(|set_pt| set_pt.value) { - let representation = self.elements.with( + let representation = self_for_effect.elements.with_untracked( |elts| elts[subject].representation ); representation.update(|rep| { @@ -428,7 +432,7 @@ impl Assembly { ) )); }); - self.realize(); + self_for_effect.realize(); } }); } -- 2.43.0 From e1952d7d5217b9d06abc0bd9734a61af637496da Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 10 Apr 2025 12:14:37 -0700 Subject: [PATCH 10/21] Clear the regulator list when switching examples --- app-proto/src/add_remove.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 573bd36..4972d3c 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -148,6 +148,7 @@ pub fn AddRemove() -> View { let assembly = &state.assembly; // clear state + assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); state.selection.update(|sel| sel.clear()); -- 2.43.0 From 4654bf06bf9e74cc99f073339a1d7cd9c0cc54ae Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 15 Apr 2025 23:44:35 -0700 Subject: [PATCH 11/21] Move half-curvature change routine into engine This routine is implemented in a very representation-specific way, so the engine seems like the best place for it. --- app-proto/src/assembly.rs | 35 ++++------------------------------- app-proto/src/engine.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 387b567..161859d 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -8,6 +8,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ engine::{ Q, + change_half_curvature, local_unif_to_std, realize_gram, sphere, @@ -401,37 +402,9 @@ impl Assembly { let representation = self_for_effect.elements.with_untracked( |elts| elts[subject].representation ); - representation.update(|rep| { - // set the sphere's half-curvature to the desired value - rep[3] = half_curv; - - // restore normalization by contracting toward the curvature - // axis - const SIZE_THRESHOLD: f64 = 1e-9; - let half_q_lt = -2.0 * half_curv * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let mut spatial = rep.fixed_rows_mut::<3>(0); - let q_sp = spatial.norm_squared(); - if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD { - spatial.copy_from_slice( - &[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()] - ); - } else { - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - spatial.scale_mut(1.0 / scaling); - rep[4] /= scaling; - } - - /* DEBUG */ - // verify normalization - let rep_for_debug = rep.clone(); - console::log_1(&JsValue::from( - format!( - "Sphere self-product after curvature change: {}", - rep_for_debug.dot(&(&*Q * &rep_for_debug)) - ) - )); - }); + representation.update( + |rep| change_half_curvature(rep, half_curv) + ); self_for_effect.realize(); } }); diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 44f44e0..869a7de 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -35,6 +35,40 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// 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 +pub fn change_half_curvature(rep: &mut DVector, half_curv: f64) { + // set the sphere's half-curvature to the desired value + rep[3] = half_curv; + + // restore normalization by contracting toward the curvature axis + const SIZE_THRESHOLD: f64 = 1e-9; + let half_q_lt = -2.0 * half_curv * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let mut spatial = rep.fixed_rows_mut::<3>(0); + let q_sp = spatial.norm_squared(); + if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD { + spatial.copy_from_slice( + &[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()] + ); + } else { + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + spatial.scale_mut(1.0 / scaling); + rep[4] /= scaling; + } + + /* DEBUG */ + // verify normalization + let rep_for_debug = rep.clone(); + console::log_1(&JsValue::from( + format!( + "Sphere self-product after curvature change: {}", + rep_for_debug.dot(&(&*Q * &rep_for_debug)) + ) + )); +} + // --- partial matrices --- pub struct MatrixEntry { -- 2.43.0 From 955220c0bc43256fe7e47b38f7f26ca2c6a77af8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 15 Apr 2025 23:49:07 -0700 Subject: [PATCH 12/21] Shadow storage variable with builder variable --- app-proto/src/assembly.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 161859d..055b9a2 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -422,16 +422,16 @@ impl Assembly { // set up the constraint problem let problem = self.elements.with_untracked(|elts| { - let mut problem_to_be = ConstraintProblem::new(elts.len()); + let mut problem = ConstraintProblem::new(elts.len()); for (_, elt) in elts { - elt.pose(&mut problem_to_be, elts); + elt.pose(&mut problem, elts); } self.regulators.with_untracked(|regs| { for (_, reg) in regs { - reg.pose(&mut problem_to_be, elts); + reg.pose(&mut problem, elts); } }); - problem_to_be + problem }); /* DEBUG */ -- 2.43.0 From ee8a01b9cb7e27a697baeb4211ed50936c1b9ebe Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 13:20:50 -0700 Subject: [PATCH 13/21] Let regulators handle their own activation This improves code organization at the cost of a little redundancy: the default implementation of `activate` doesn't do anything, and its implementation for `HalfCurvatureRegulator` redundantly accesses the set point signal and checks whether the regulator is set. --- app-proto/src/assembly.rs | 58 ++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 055b9a2..aedc344 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -145,6 +145,8 @@ pub trait Regulator: ProblemPoser + OutlineItem { fn subjects(&self) -> Vec; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; + + fn activate(&self, _assembly: &Assembly) {} } pub struct ProductRegulator { @@ -202,6 +204,17 @@ impl Regulator for HalfCurvatureRegulator { fn set_point(&self) -> Signal { self.set_point } + + fn activate(&self, assembly: &Assembly) { + if let Some(half_curv) = self.set_point.with_untracked(|set_pt| set_pt.value) { + let representation = assembly.elements.with_untracked( + |elts| elts[self.subject].representation + ); + representation.update( + |rep| change_half_curvature(rep, half_curv) + ); + } + } } impl ProblemPoser for HalfCurvatureRegulator { @@ -313,7 +326,7 @@ impl Assembly { fn insert_regulator(&self, regulator: Rc) { let subjects = regulator.subjects(); let key = self.regulators.update( - |regs| regs.insert(regulator) + |regs| regs.insert(regulator.clone()) ); let subject_regulators: Vec<_> = self.elements.with_untracked( |elts| subjects.into_iter().map( @@ -324,6 +337,19 @@ impl Assembly { regulators.update(|regs| regs.insert(key)); } + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + let self_for_effect = self.clone(); + create_effect(move || { + console::log_1(&JsValue::from( + format!("Updated regulator with subjects {:?}", regulator.subjects()) + )); + if regulator.set_point().with(|set_pt| set_pt.is_present()) { + regulator.activate(&self_for_effect); + self_for_effect.realize(); + } + }); + /* DEBUG */ // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); @@ -365,18 +391,6 @@ impl Assembly { measurement: measurement, set_point: set_point })); - - // update the realization when the regulator becomes a constraint, or is - // edited while acting as a constraint - let self_for_effect = self.clone(); - create_effect(move || { - console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", subjects) - )); - if set_point.with(|set_pt| set_pt.is_present()) { - self_for_effect.realize(); - } - }); } pub fn insert_new_half_curvature_regulator(&self, subject: ElementKey) { @@ -390,24 +404,6 @@ impl Assembly { measurement: measurement, set_point: set_point })); - - // update the realization when the regulator becomes a constraint, or is - // edited while acting as a constraint - let self_for_effect = self.clone(); - create_effect(move || { - console::log_1(&JsValue::from( - format!("Updated regulator with subjects [{}]", subject) - )); - if let Some(half_curv) = set_point.with(|set_pt| set_pt.value) { - let representation = self_for_effect.elements.with_untracked( - |elts| elts[subject].representation - ); - representation.update( - |rep| change_half_curvature(rep, half_curv) - ); - self_for_effect.realize(); - } - }); } // --- realization --- -- 2.43.0 From 8f8e806d123b1d38ec37b6e09547b2ef8323b2b4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 14:02:37 -0700 Subject: [PATCH 14/21] Move pointer creation into `insert_regulator` This will make it easier to give each regulator a constructor instead of an "insert new" method. --- app-proto/src/assembly.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index aedc344..be52461 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -323,11 +323,15 @@ impl Assembly { ); } - fn insert_regulator(&self, regulator: Rc) { - let subjects = regulator.subjects(); + fn insert_regulator(&self, regulator: T) { + // add the regulator to the assembly's regulator list + let regulator_rc = Rc::new(regulator); let key = self.regulators.update( - |regs| regs.insert(regulator.clone()) + |regs| regs.insert(regulator_rc.clone()) ); + + // add the regulator to each subject's regulator list + let subjects = regulator_rc.subjects(); let subject_regulators: Vec<_> = self.elements.with_untracked( |elts| subjects.into_iter().map( |subj| elts[subj].regulators @@ -342,10 +346,10 @@ impl Assembly { let self_for_effect = self.clone(); create_effect(move || { console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator.subjects()) + format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) )); - if regulator.set_point().with(|set_pt| set_pt.is_present()) { - regulator.activate(&self_for_effect); + if regulator_rc.set_point().with(|set_pt| set_pt.is_present()) { + regulator_rc.activate(&self_for_effect); self_for_effect.realize(); } }); @@ -386,11 +390,11 @@ impl Assembly { } ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(Rc::new(ProductRegulator { + self.insert_regulator(ProductRegulator { subjects: subjects, measurement: measurement, set_point: set_point - })); + }); } pub fn insert_new_half_curvature_regulator(&self, subject: ElementKey) { @@ -399,11 +403,11 @@ impl Assembly { move |elts| elts[subject].representation.with(|rep| rep[3]) ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(Rc::new(HalfCurvatureRegulator { + self.insert_regulator(HalfCurvatureRegulator { subject: subject, measurement: measurement, set_point: set_point - })); + }); } // --- realization --- -- 2.43.0 From 52d99755f972d8a6fb5689f0a8e022687162e9fd Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 14:10:07 -0700 Subject: [PATCH 15/21] Give each regulator a constructor The assembly shouldn't have to know how to construct regulators. --- app-proto/src/add_remove.rs | 6 ++- app-proto/src/assembly.rs | 77 ++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 4972d3c..deac2bb 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -4,7 +4,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element} + assembly::{Assembly, Element, ProductRegulator} }; /* DEBUG */ @@ -189,7 +189,9 @@ pub fn AddRemove() -> View { .try_into() .unwrap() ); - state.assembly.insert_new_product_regulator(subjects); + state.assembly.insert_regulator( + ProductRegulator::new(subjects, &state.assembly) + ); state.selection.update(|sel| sel.clear()); } ) { "🔗" } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index be52461..0eb3e64 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -155,6 +155,29 @@ pub struct ProductRegulator { pub set_point: Signal } +impl ProductRegulator { + pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> ProductRegulator { + let measurement = assembly.elements.map( + move |elts| { + let representations = subjects.map(|subj| elts[subj].representation); + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) + ) + ) + } + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + ProductRegulator { + subjects: subjects, + measurement: measurement, + set_point: set_point + } + } +} + impl Regulator for ProductRegulator { fn subjects(&self) -> Vec { self.subjects.into() @@ -192,6 +215,23 @@ pub struct HalfCurvatureRegulator { pub set_point: Signal } +impl HalfCurvatureRegulator { + pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { + const CURVATURE_COMPONENT: usize = 3; + let measurement = assembly.elements.map( + move |elts| elts[subject].representation.with(|rep| rep[CURVATURE_COMPONENT]) + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + HalfCurvatureRegulator { + subject: subject, + measurement: measurement, + set_point: set_point + } + } +} + impl Regulator for HalfCurvatureRegulator { fn subjects(&self) -> Vec { vec![self.subject] @@ -285,7 +325,7 @@ impl Assembly { self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); // regulate the sphere's curvature - self.insert_new_half_curvature_regulator(key); + self.insert_regulator(HalfCurvatureRegulator::new(key, &self)); key } @@ -323,7 +363,7 @@ impl Assembly { ); } - fn insert_regulator(&self, regulator: T) { + pub fn insert_regulator(&self, regulator: T) { // add the regulator to the assembly's regulator list let regulator_rc = Rc::new(regulator); let key = self.regulators.update( @@ -377,39 +417,6 @@ impl Assembly { }); } - pub fn insert_new_product_regulator(&self, subjects: [ElementKey; 2]) { - // create and insert a new product regulator - let measurement = self.elements.map( - move |elts| { - let representations = subjects.map(|subj| elts[subj].representation); - representations[0].with(|rep_0| - representations[1].with(|rep_1| - rep_0.dot(&(&*Q * rep_1)) - ) - ) - } - ); - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(ProductRegulator { - subjects: subjects, - measurement: measurement, - set_point: set_point - }); - } - - pub fn insert_new_half_curvature_regulator(&self, subject: ElementKey) { - // create and insert a new half-curvature regulator - let measurement = self.elements.map( - move |elts| elts[subject].representation.with(|rep| rep[3]) - ); - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(HalfCurvatureRegulator { - subject: subject, - measurement: measurement, - set_point: set_point - }); - } - // --- realization --- pub fn realize(&self) { -- 2.43.0 From 7f21e7e999c965c793491afe5e1a67cfe8cbc4ea Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 14:16:54 -0700 Subject: [PATCH 16/21] Centralize the curvature component index constant --- app-proto/src/assembly.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0eb3e64..f14f0c4 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -58,6 +58,8 @@ pub struct Element { } impl Element { + const CURVATURE_COMPONENT: usize = 3; + pub fn new( id: String, label: String, @@ -217,9 +219,10 @@ pub struct HalfCurvatureRegulator { impl HalfCurvatureRegulator { pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { - const CURVATURE_COMPONENT: usize = 3; let measurement = assembly.elements.map( - move |elts| elts[subject].representation.with(|rep| rep[CURVATURE_COMPONENT]) + move |elts| elts[subject].representation.with( + |rep| rep[Element::CURVATURE_COMPONENT] + ) ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); @@ -262,8 +265,7 @@ impl ProblemPoser for HalfCurvatureRegulator { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { if let Some(col) = elts[self.subject].column_index { - const CURVATURE_COMPONENT: usize = 3; - problem.frozen.push(CURVATURE_COMPONENT, col, val); + problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); } else { panic!("Tried to write problem data from a regulator with an unindexed subject"); } -- 2.43.0 From 620a6be918cc1a860aaa1f3e217db3fe32d4e8db Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 14:40:28 -0700 Subject: [PATCH 17/21] Make regulator naming more consistent Although the inversive distance regulator mostly acts as a general Lorentz product regulator, calling it `InversiveDistanceRegulator` explains how we intend to use it and demonstrates our naming convention. --- app-proto/src/add_remove.rs | 4 ++-- app-proto/src/assembly.rs | 14 +++++++------- app-proto/src/outline.rs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index deac2bb..14fcd41 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -4,7 +4,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element, ProductRegulator} + assembly::{Assembly, Element, InversiveDistanceRegulator} }; /* DEBUG */ @@ -190,7 +190,7 @@ pub fn AddRemove() -> View { .unwrap() ); state.assembly.insert_regulator( - ProductRegulator::new(subjects, &state.assembly) + InversiveDistanceRegulator::new(subjects, &state.assembly) ); state.selection.update(|sel| sel.clear()); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index f14f0c4..5dae508 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -151,14 +151,14 @@ pub trait Regulator: ProblemPoser + OutlineItem { fn activate(&self, _assembly: &Assembly) {} } -pub struct ProductRegulator { +pub struct InversiveDistanceRegulator { pub subjects: [ElementKey; 2], pub measurement: ReadSignal, pub set_point: Signal } -impl ProductRegulator { - pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> ProductRegulator { +impl InversiveDistanceRegulator { + pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { let measurement = assembly.elements.map( move |elts| { let representations = subjects.map(|subj| elts[subj].representation); @@ -172,7 +172,7 @@ impl ProductRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); - ProductRegulator { + InversiveDistanceRegulator { subjects: subjects, measurement: measurement, set_point: set_point @@ -180,7 +180,7 @@ impl ProductRegulator { } } -impl Regulator for ProductRegulator { +impl Regulator for InversiveDistanceRegulator { fn subjects(&self) -> Vec { self.subjects.into() } @@ -194,7 +194,7 @@ impl Regulator for ProductRegulator { } } -impl ProblemPoser for ProductRegulator { +impl ProblemPoser for InversiveDistanceRegulator { fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { @@ -633,7 +633,7 @@ mod tests { ) }); elts[subjects[0]].column_index = Some(0); - ProductRegulator { + InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 32ff2e7..2446337 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -13,7 +13,7 @@ use crate::{ assembly::{ ElementKey, HalfCurvatureRegulator, - ProductRegulator, + InversiveDistanceRegulator, Regulator, RegulatorKey }, @@ -94,7 +94,7 @@ pub trait OutlineItem { fn outline_item(self: Rc, element_key: ElementKey) -> View; } -impl OutlineItem for ProductRegulator { +impl OutlineItem for InversiveDistanceRegulator { fn outline_item(self: Rc, element_key: ElementKey) -> View { let state = use_context::(); let other_subject = if self.subjects[0] == element_key { -- 2.43.0 From 8dab223f6ab82af1160a0062c4c5ead247f8c6b8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 21:00:24 -0700 Subject: [PATCH 18/21] Use field init shorthand in regulator constructors --- app-proto/src/assembly.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 5dae508..1850771 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -172,11 +172,7 @@ impl InversiveDistanceRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); - InversiveDistanceRegulator { - subjects: subjects, - measurement: measurement, - set_point: set_point - } + InversiveDistanceRegulator { subjects, measurement, set_point } } } @@ -227,11 +223,7 @@ impl HalfCurvatureRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); - HalfCurvatureRegulator { - subject: subject, - measurement: measurement, - set_point: set_point - } + HalfCurvatureRegulator { subject, measurement, set_point } } } -- 2.43.0 From 5506ec1f43bdeceb995af792200600e703823272 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 21:25:22 -0700 Subject: [PATCH 19/21] Make regulator activation less redundant --- app-proto/src/assembly.rs | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 1850771..68a2863 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -148,7 +148,15 @@ pub trait Regulator: ProblemPoser + OutlineItem { fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; - fn activate(&self, _assembly: &Assembly) {} + // this method is used to responsively precondition the assembly for + // realization when the regulator becomes a constraint, or is edited while + // acting as a constraint. it should track the set point, do any desired + // preconditioning when the set point is present, and use its return value + // to report whether the set is present. the default implementation does no + // preconditioning + fn try_activate(&self, _assembly: &Assembly) -> bool { + self.set_point().with(|set_pt| set_pt.is_present()) + } } pub struct InversiveDistanceRegulator { @@ -240,14 +248,18 @@ impl Regulator for HalfCurvatureRegulator { self.set_point } - fn activate(&self, assembly: &Assembly) { - if let Some(half_curv) = self.set_point.with_untracked(|set_pt| set_pt.value) { - let representation = assembly.elements.with_untracked( - |elts| elts[self.subject].representation - ); - representation.update( - |rep| change_half_curvature(rep, half_curv) - ); + fn try_activate(&self, assembly: &Assembly) -> bool { + match self.set_point.with(|set_pt| set_pt.value) { + Some(half_curv) => { + let representation = assembly.elements.with_untracked( + |elts| elts[self.subject].representation + ); + representation.update( + |rep| change_half_curvature(rep, half_curv) + ); + true + } + None => false } } } @@ -382,8 +394,7 @@ impl Assembly { console::log_1(&JsValue::from( format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) )); - if regulator_rc.set_point().with(|set_pt| set_pt.is_present()) { - regulator_rc.activate(&self_for_effect); + if regulator_rc.try_activate(&self_for_effect) { self_for_effect.realize(); } }); -- 2.43.0 From 99a9c3ec55921b81550ec663eeefb8a02dec2804 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 17 Apr 2025 21:31:24 -0700 Subject: [PATCH 20/21] Flag regulator update logging as debug --- app-proto/src/assembly.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 68a2863..615b010 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -391,9 +391,12 @@ impl Assembly { // edited while acting as a constraint let self_for_effect = self.clone(); create_effect(move || { + /* DEBUG */ + // log the regulator update console::log_1(&JsValue::from( format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) )); + if regulator_rc.try_activate(&self_for_effect) { self_for_effect.realize(); } -- 2.43.0 From 5eeb0935ca7d210358c086f31931cc0c5281c73d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 21 Apr 2025 14:47:26 -0700 Subject: [PATCH 21/21] Change conditional panic to `expect` Use `expect` to communicate that every element should have a column index when `pose` is called. Rewrite the panic messages in the style recommended by the `expect` documentation. --- app-proto/src/assembly.rs | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 615b010..5c926ca 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -134,12 +134,11 @@ impl Element { impl ProblemPoser for Element { fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { - if let Some(index) = self.column_index { - problem.gram.push_sym(index, index, 1.0); - problem.guess.set_column(index, &self.representation.get_clone_untracked()); - } else { - panic!("Tried to write problem data from an unindexed element: \"{}\"", self.id); - } + let index = self.column_index.expect( + format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str() + ); + problem.gram.push_sym(index, index, 1.0); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } @@ -202,14 +201,12 @@ impl ProblemPoser for InversiveDistanceRegulator { fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let subject_column_indices = self.subjects.map( - |subj| elts[subj].column_index + let [row, col] = self.subjects.map( + |subj| elts[subj].column_index.expect( + "Subjects should be indexed before inversive distance regulator writes problem data" + ) ); - if let [Some(row), Some(col)] = subject_column_indices { - problem.gram.push_sym(row, col, val); - } else { - panic!("Tried to write problem data from a regulator with an unindexed subject"); - } + problem.gram.push_sym(row, col, val); } }); } @@ -268,11 +265,10 @@ impl ProblemPoser for HalfCurvatureRegulator { fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - if let Some(col) = elts[self.subject].column_index { - problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); - } else { - panic!("Tried to write problem data from a regulator with an unindexed subject"); - } + let col = elts[self.subject].column_index.expect( + "Subject should be indexed before half-curvature regulator writes problem data" + ); + problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); } }); } @@ -611,7 +607,7 @@ mod tests { use super::*; #[test] - #[should_panic(expected = "Tried to write problem data from an unindexed element: \"sphere\"")] + #[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { let _ = create_root(|| { Element::new( @@ -624,8 +620,8 @@ mod tests { } #[test] - #[should_panic(expected = "Tried to write problem data from a regulator with an unindexed subject")] - fn unindexed_subject_test() { + #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] + fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { let mut elts = Slab::new(); let subjects = [0, 1].map(|k| { -- 2.43.0