From af6c40817fba0f4e9fa07c628edc34a9941d02d3 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 20 Sep 2025 00:51:26 -0700 Subject: [PATCH 01/11] feat: Point coordinate regulators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements regulators for the Euclidean coordinates of Point entities, automatically creating all three of them for each added point entity. When such a regulator is set, it freezes the corresponding representation coordinate to the set point. In addition, if all three coordinates of a given Point are set, the coradius coordinate (which holds the norm of the point) is frozen as well. Note that a PointCoordinateRegulator must be created with a Point as the subject. This commit modifies HalfCurvatureRegulator analogously, so that it can only be created with a Sphere. A couple of prospective issues that should be filed in association with this commit: * The new coordinate regulators create redundant display information with the raw representation coordinates of a point that are already shown in the outline view. * The optimization status of these regulators together with HalfCurvature regulators (i.e., the ones implemented by freezing coordinates) is different from InversiveDistance regulators when an Assembly is unrealizable: the frozen-coordinate constraints will be "hard" in that they will be forced to precisely equal their set point, whereas the distance regulators are "soft" in that they can be relaxed from their set points in an effort to minimize the loss function of the configuration as compared to the values of the constraints. Perhaps at some point we should/will have a mechanism to specify the softness/hardness of constraints, but in the meantime, there should not be two different categories of constraints. Suppose we decide that by default that all constraints are soft. Then the optimizer should be able to search changing, for example, the radius of a curvature-constrained sphere, so as to minimize the loss function (for a loss that would therefore presumably have a term akin to the square of the difference between the specified and actual half-curvature of the sphere). For example, suppose you specify that the half-curvature of a sphere is 1 (so it has radius 1/2) but that its distance to a point is -1. These constraints cannot be satisfied, so the optimization fails, presumably with the point at the sphere center, and the sphere with radius 1/2. So all of the loss is concentrated in the difference between the actual point-sphere distance being -1/2, not -1. It would be more appropriate (in the all-soft constraint regime) to end up at something like a sphere of half-curvature 1/√2 with the point at the center, so that the loss is split between both the half-curvature and the distance to the sphere being off by 1 - 1/√2. (At a guess, that would minimize the sum of the squares of the two differences.) --- app-proto/src/assembly.rs | 2 +- app-proto/src/components/outline.rs | 1 + app-proto/src/engine.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0264b75..aa79fe1 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -517,8 +517,8 @@ impl ProblemPoser for HalfCurvatureRegulator { } #[derive(Clone, Copy, Sequence)] -pub enum Axis { X = 0, Y = 1, Z = 2 } +pub enum Axis { X = 0, Y = 1, Z = 2 } impl Axis { fn name(&self) -> &'static str { match self { Axis::X => "X", Axis::Y => "Y", Axis::Z => "Z" } diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs index 547b73b..a4d2e5b 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/components/outline.rs @@ -6,6 +6,7 @@ use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use crate::{ AppState, assembly::{ + Axis, Element, HalfCurvatureRegulator, InversiveDistanceRegulator, diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 0f26f02..feb23cf 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -46,7 +46,7 @@ pub fn project_sphere_to_normalized(rep: &mut DVector) { // normalize a point's representation vector by scaling pub fn project_point_to_normalized(rep: &mut DVector) { - rep.scale_mut(0.5 / rep[3]); + rep.scale_mut(0.5 / rep[3]); //FIXME: This 3 should be Point::WEIGHT_COMPONENT } // --- partial matrices --- -- 2.43.0 From a614098b22c9ba489cfced9bf92cac7317750928 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 6 Oct 2025 16:31:02 -0600 Subject: [PATCH 02/11] chore: typographical improvements per review --- app-proto/src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index feb23cf..0f26f02 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -46,7 +46,7 @@ pub fn project_sphere_to_normalized(rep: &mut DVector) { // normalize a point's representation vector by scaling pub fn project_point_to_normalized(rep: &mut DVector) { - rep.scale_mut(0.5 / rep[3]); //FIXME: This 3 should be Point::WEIGHT_COMPONENT + rep.scale_mut(0.5 / rep[3]); } // --- partial matrices --- -- 2.43.0 From 6ad3ed1176cac50b513109c5018603416a88acb9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 7 Oct 2025 15:36:12 -0700 Subject: [PATCH 03/11] Streamline axis naming This makes it simpler, from the programmer's perspective, to get the name of an axis as a string slice and to format an axis name into a string. To me, the matching method `Axis::name` seems more direct than the explicit lookup table that it replaces, and I'm hoping that it will be about as easy for the compiler to inline, or even easier. Implementing `Display` enables us to hand an `Axis` to a string formatter without any explicit conversion. It adds extra code in the short run, but I'd expect it to simplify our code in the long run by fitting into the conventions set by the Rust standard library. --- app-proto/src/assembly.rs | 1 - app-proto/src/components/outline.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index aa79fe1..d571d0e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -517,7 +517,6 @@ impl ProblemPoser for HalfCurvatureRegulator { } #[derive(Clone, Copy, Sequence)] - pub enum Axis { X = 0, Y = 1, Z = 2 } impl Axis { fn name(&self) -> &'static str { diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs index a4d2e5b..547b73b 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/components/outline.rs @@ -6,7 +6,6 @@ use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use crate::{ AppState, assembly::{ - Axis, Element, HalfCurvatureRegulator, InversiveDistanceRegulator, -- 2.43.0 From 5ee24aa91da299299a474ae8a2ecc2d4c2c00219 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 7 Oct 2025 16:19:14 -0700 Subject: [PATCH 04/11] Spruce up formatting and error messages Make the new code's formatting and error messages more consistent with the previous code. I don't necessarily have a strong preference for the previous conventions, but I do like stuff to be consistent. --- app-proto/src/assembly.rs | 42 ++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index d571d0e..5ba75ff 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -312,13 +312,14 @@ impl Element for Point { point(0.0, 0.0, 0.0), ) } - + fn default_regulators(self: Rc) -> Vec> { all::() - .map(|axis| { - Rc::new(PointCoordinateRegulator::new(self.clone(), axis)) - as Rc:: - }) + .map( + |axis| Rc::new( + PointCoordinateRegulator::new(self.clone(), axis) + ) as Rc:: + ) .collect() } @@ -543,19 +544,32 @@ impl PointCoordinateRegulator { let measurement = subject.representation().map( move |rep| rep[axis as usize] ); + let set_point = create_signal(SpecifiedValue::from_empty_spec()); - Self { subject, axis, measurement, set_point, serial: Self::next_serial() } + let serial = Self::next_serial(); + + Self { subject, axis, measurement, set_point, serial } } } impl Serial for PointCoordinateRegulator { - fn serial(&self) -> u64 { self.serial } + fn serial(&self) -> u64 { + self.serial + } } impl Regulator for PointCoordinateRegulator { - fn subjects(&self) -> Vec> { vec![self.subject.clone()] } - fn measurement(&self) -> ReadSignal { self.measurement } - fn set_point(&self) -> Signal { self.set_point } + fn subjects(&self) -> Vec> { + vec![self.subject.clone()] + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } } impl ProblemPoser for PointCoordinateRegulator { @@ -576,10 +590,10 @@ impl ProblemPoser for PointCoordinateRegulator { coords[index.0] = value } } - if nset == Axis::CARDINALITY { - let [x, y, z] = coords; - problem.frozen.push( - Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]); + if n_set == Axis::CARDINALITY { + let [x, y, z] = coords_frozen; + let norm = point(x, y, z)[Point::NORM_COMPONENT]; + problem.frozen.push(Point::NORM_COMPONENT, col, norm); } } }); -- 2.43.0 From 1f604eb29ad551bf0229688ee844ae028dd525cc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 9 Oct 2025 22:36:37 -0700 Subject: [PATCH 05/11] Revert "Spruce up formatting and error messages" This reverts commit adc60ac5c160ceee11ee205352a1665b29fc6e94. We decided that it would be better for me to request formatting changes one by one. --- app-proto/src/assembly.rs | 42 +++++++++++++-------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 5ba75ff..d571d0e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -312,14 +312,13 @@ impl Element for Point { point(0.0, 0.0, 0.0), ) } - + fn default_regulators(self: Rc) -> Vec> { all::() - .map( - |axis| Rc::new( - PointCoordinateRegulator::new(self.clone(), axis) - ) as Rc:: - ) + .map(|axis| { + Rc::new(PointCoordinateRegulator::new(self.clone(), axis)) + as Rc:: + }) .collect() } @@ -544,32 +543,19 @@ impl PointCoordinateRegulator { let measurement = subject.representation().map( move |rep| rep[axis as usize] ); - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - let serial = Self::next_serial(); - - Self { subject, axis, measurement, set_point, serial } + Self { subject, axis, measurement, set_point, serial: Self::next_serial() } } } impl Serial for PointCoordinateRegulator { - fn serial(&self) -> u64 { - self.serial - } + fn serial(&self) -> u64 { self.serial } } impl Regulator for PointCoordinateRegulator { - fn subjects(&self) -> Vec> { - vec![self.subject.clone()] - } - - fn measurement(&self) -> ReadSignal { - self.measurement - } - - fn set_point(&self) -> Signal { - self.set_point - } + fn subjects(&self) -> Vec> { vec![self.subject.clone()] } + fn measurement(&self) -> ReadSignal { self.measurement } + fn set_point(&self) -> Signal { self.set_point } } impl ProblemPoser for PointCoordinateRegulator { @@ -590,10 +576,10 @@ impl ProblemPoser for PointCoordinateRegulator { coords[index.0] = value } } - if n_set == Axis::CARDINALITY { - let [x, y, z] = coords_frozen; - let norm = point(x, y, z)[Point::NORM_COMPONENT]; - problem.frozen.push(Point::NORM_COMPONENT, col, norm); + if nset == Axis::CARDINALITY { + let [x, y, z] = coords; + problem.frozen.push( + Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]); } } }); -- 2.43.0 From 040b080d2bf6b54264c460cfe0092c9783b9381e Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 10 Oct 2025 09:39:57 -0700 Subject: [PATCH 06/11] chore: uniformize error messages, 80-char lines, import fmt::Display --- app-proto/src/assembly.rs | 58 +++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index d571d0e..2a734cc 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -256,12 +256,19 @@ impl Serial for Sphere { } } +fn index_message(thing: &str, name: &str, actor: &str) -> String { + format!( + "{thing} \"{name}\" must be indexed before {actor} writes problem data" + ) +} + impl ProblemPoser for Sphere { fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( indexing_error("Sphere", &self.id, "it").as_str()); problem.gram.push_sym(index, index, 1.0); - problem.guess.set_column(index, &self.representation.get_clone_untracked()); + problem.guess.set_column( + index, &self.representation.get_clone_untracked()); } } @@ -367,7 +374,8 @@ impl ProblemPoser for Point { indexing_error("Point", &self.id, "it").as_str()); problem.gram.push_sym(index, index, 0.0); problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); - problem.guess.set_column(index, &self.representation.get_clone_untracked()); + problem.guess.set_column( + index, &self.representation.get_clone_untracked()); } } @@ -412,7 +420,8 @@ pub struct InversiveDistanceRegulator { impl InversiveDistanceRegulator { pub fn new(subjects: [Rc; 2]) -> Self { - let representations = subjects.each_ref().map(|subj| subj.representation()); + let representations = subjects.each_ref().map( + |subj| subj.representation()); let measurement = create_memo(move || { representations[0].with(|rep_0| representations[1].with(|rep_1| @@ -544,7 +553,9 @@ impl PointCoordinateRegulator { move |rep| rep[axis as usize] ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - Self { subject, axis, measurement, set_point, serial: Self::next_serial() } + Self { + subject, axis, measurement, set_point, serial: Self::next_serial() + } } } @@ -579,7 +590,9 @@ impl ProblemPoser for PointCoordinateRegulator { if nset == Axis::CARDINALITY { let [x, y, z] = coords; problem.frozen.push( - Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]); + Point::NORM_COMPONENT, + col, + point(x,y,z)[Point::NORM_COMPONENT]); } } }); @@ -678,7 +691,8 @@ impl Assembly { let id = elt.id().clone(); let elt_rc = Rc::new(elt); self.elements.update(|elts| elts.insert(elt_rc.clone())); - self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); + self.elements_by_id.update( + |elts_by_id| elts_by_id.insert(id, elt_rc.clone())); // create and insert the element's default regulators for reg in elt_rc.default_regulators() { @@ -754,7 +768,8 @@ impl Assembly { pub fn load_config(&self, config: &DMatrix) { for elt in self.elements.get_clone_untracked() { elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) + |rep| rep.set_column( + 0, &config.column(elt.column_index().unwrap())) ); } } @@ -899,7 +914,8 @@ impl Assembly { if column_index < realized_dim { // this element had a column index when we started, so by // invariant (1), it's reflected in the tangent space - let mut target_columns = motion_proj.columns_mut(0, realized_dim); + let mut target_columns = + motion_proj.columns_mut(0, realized_dim); target_columns += self.tangent.with( |tan| tan.proj(&elt_motion.velocity, column_index) ); @@ -907,9 +923,10 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - let unif_to_std = elt_motion.element.representation().with_untracked( - |rep| local_unif_to_std(rep.as_view()) - ); + let unif_to_std = elt_motion + .element.representation() + .with_untracked( + |rep| local_unif_to_std(rep.as_view())); target_column += unif_to_std * elt_motion.velocity; } } @@ -926,7 +943,9 @@ impl Assembly { elt.project_to_normalized(rep); }, None => { - console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) + console_log!( + "No velocity to unpack for fresh element \"{}\"", + elt.id()) }, }; }); @@ -961,13 +980,15 @@ mod tests { fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { let subjects = [0, 1].map( - |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc + |k| Rc::new(Sphere::default( + format!("sphere{k}"), k)) as Rc ); subjects[0].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), - set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), + set_point: create_signal( + SpecifiedValue::try_from("0.0".to_string()).unwrap()), serial: InversiveDistanceRegulator::next_serial() }.pose(&mut ConstraintProblem::new(2)); }); @@ -996,8 +1017,10 @@ mod tests { // nudge the sphere repeatedly along the `z` axis const STEP_SIZE: f64 = 0.0025; const STEP_CNT: usize = 400; - let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); - let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); + let sphere = assembly.elements_by_id.with( + |elts_by_id| elts_by_id[sphere_id].clone()); + let velocity = + DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); for _ in 0..STEP_CNT { assembly.deform( vec![ @@ -1015,7 +1038,8 @@ mod tests { let final_half_curv = sphere.representation().with_untracked( |rep| rep[Sphere::CURVATURE_COMPONENT] ); - assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); + assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() + < DRIFT_TOL); }); } } -- 2.43.0 From d68f0d2b152f4dac104088f687d0a7c64a83df06 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 10 Oct 2025 18:05:09 -0700 Subject: [PATCH 07/11] chore: revert 80-character limit, reorder and rename index message helper --- app-proto/src/assembly.rs | 62 ++++++++++++--------------------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 2a734cc..15a7211 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -256,19 +256,12 @@ impl Serial for Sphere { } } -fn index_message(thing: &str, name: &str, actor: &str) -> String { - format!( - "{thing} \"{name}\" must be indexed before {actor} writes problem data" - ) -} - impl ProblemPoser for Sphere { fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( indexing_error("Sphere", &self.id, "it").as_str()); problem.gram.push_sym(index, index, 1.0); - problem.guess.set_column( - index, &self.representation.get_clone_untracked()); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } @@ -374,8 +367,7 @@ impl ProblemPoser for Point { indexing_error("Point", &self.id, "it").as_str()); problem.gram.push_sym(index, index, 0.0); problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); - problem.guess.set_column( - index, &self.representation.get_clone_untracked()); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } @@ -420,8 +412,7 @@ pub struct InversiveDistanceRegulator { impl InversiveDistanceRegulator { pub fn new(subjects: [Rc; 2]) -> Self { - let representations = subjects.each_ref().map( - |subj| subj.representation()); + let representations = subjects.each_ref().map(|subj| subj.representation()); let measurement = create_memo(move || { representations[0].with(|rep_0| representations[1].with(|rep_1| @@ -553,9 +544,7 @@ impl PointCoordinateRegulator { move |rep| rep[axis as usize] ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - Self { - subject, axis, measurement, set_point, serial: Self::next_serial() - } + Self { subject, axis, measurement, set_point, serial: Self::next_serial() } } } @@ -590,9 +579,7 @@ impl ProblemPoser for PointCoordinateRegulator { if nset == Axis::CARDINALITY { let [x, y, z] = coords; problem.frozen.push( - Point::NORM_COMPONENT, - col, - point(x,y,z)[Point::NORM_COMPONENT]); + Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]); } } }); @@ -691,8 +678,7 @@ impl Assembly { let id = elt.id().clone(); let elt_rc = Rc::new(elt); self.elements.update(|elts| elts.insert(elt_rc.clone())); - self.elements_by_id.update( - |elts_by_id| elts_by_id.insert(id, elt_rc.clone())); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); // create and insert the element's default regulators for reg in elt_rc.default_regulators() { @@ -701,9 +687,7 @@ impl Assembly { } pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { - let can_insert = self.elements_by_id.with_untracked( - |elts_by_id| !elts_by_id.contains_key(elt.id()) - ); + 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); } @@ -768,8 +752,7 @@ impl Assembly { pub fn load_config(&self, config: &DMatrix) { for elt in self.elements.get_clone_untracked() { elt.representation().update( - |rep| rep.set_column( - 0, &config.column(elt.column_index().unwrap())) + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) ); } } @@ -914,8 +897,7 @@ impl Assembly { if column_index < realized_dim { // this element had a column index when we started, so by // invariant (1), it's reflected in the tangent space - let mut target_columns = - motion_proj.columns_mut(0, realized_dim); + let mut target_columns = motion_proj.columns_mut(0, realized_dim); target_columns += self.tangent.with( |tan| tan.proj(&elt_motion.velocity, column_index) ); @@ -923,10 +905,9 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - let unif_to_std = elt_motion - .element.representation() - .with_untracked( - |rep| local_unif_to_std(rep.as_view())); + let unif_to_std = elt_motion.element.representation().with_untracked( + |rep| local_unif_to_std(rep.as_view()) + ); target_column += unif_to_std * elt_motion.velocity; } } @@ -943,9 +924,7 @@ impl Assembly { elt.project_to_normalized(rep); }, None => { - console_log!( - "No velocity to unpack for fresh element \"{}\"", - elt.id()) + console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) }, }; }); @@ -980,15 +959,13 @@ mod tests { fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { let subjects = [0, 1].map( - |k| Rc::new(Sphere::default( - format!("sphere{k}"), k)) as Rc + |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc ); subjects[0].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), - set_point: create_signal( - SpecifiedValue::try_from("0.0".to_string()).unwrap()), + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), serial: InversiveDistanceRegulator::next_serial() }.pose(&mut ConstraintProblem::new(2)); }); @@ -1017,10 +994,8 @@ mod tests { // nudge the sphere repeatedly along the `z` axis const STEP_SIZE: f64 = 0.0025; const STEP_CNT: usize = 400; - let sphere = assembly.elements_by_id.with( - |elts_by_id| elts_by_id[sphere_id].clone()); - let velocity = - DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); + let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); + let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); for _ in 0..STEP_CNT { assembly.deform( vec![ @@ -1038,8 +1013,7 @@ mod tests { let final_half_curv = sphere.representation().with_untracked( |rep| rep[Sphere::CURVATURE_COMPONENT] ); - assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() - < DRIFT_TOL); + assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); }); } } -- 2.43.0 From a4b355d943236ea7213ace70c8fd5a90077fe6c5 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 10 Oct 2025 22:57:52 -0700 Subject: [PATCH 08/11] chore: Hopefully final formatting items from review --- app-proto/src/assembly.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 15a7211..d571d0e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -687,7 +687,9 @@ impl Assembly { } pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { - let can_insert = self.elements_by_id.with_untracked(|elts_by_id| !elts_by_id.contains_key(elt.id())); + 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); } -- 2.43.0 From 3635abc562df94b6439e1e7b577530066ab44e45 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 10 Oct 2025 10:20:38 -0700 Subject: [PATCH 09/11] chore: remove trailing whitespace, add CR at end of file --- app-proto/src/assembly.rs | 162 +++++++++--------- app-proto/src/components/diagnostics.rs | 30 ++-- app-proto/src/components/display.rs | 160 ++++++++--------- app-proto/src/components/outline.rs | 16 +- app-proto/src/components/point.frag | 4 +- app-proto/src/components/point.vert | 6 +- app-proto/src/components/spheres.frag | 24 +-- .../src/components/test_assembly_chooser.rs | 88 +++++----- app-proto/src/engine.rs | 136 +++++++-------- app-proto/src/main.rs | 8 +- app-proto/src/specified.rs | 6 +- 11 files changed, 320 insertions(+), 320 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index d571d0e..9b177b5 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -45,7 +45,7 @@ static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0); pub trait Serial { // a serial number that uniquely identifies this element fn serial(&self) -> u64; - + // take the next serial number, panicking if that was the last one left fn next_serial() -> u64 where Self: Sized { // the technique we use to panic on overflow is taken from _Rust Atomics @@ -101,33 +101,33 @@ pub trait ProblemPoser { pub trait Element: Serial + ProblemPoser + DisplayItem { // the default identifier for an element of this type fn default_id() -> String where Self: Sized; - + // the default example of an element of this type fn default(id: String, id_num: u64) -> Self where Self: Sized; - + // the default regulators that come with this element fn default_regulators(self: Rc) -> Vec> { Vec::new() } - + fn id(&self) -> &String; fn label(&self) -> &String; fn representation(&self) -> Signal>; fn ghost(&self) -> Signal; - + // the regulators the element is subject to. the assembly that owns the // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; - + // project a representation vector for this kind of element onto its // normalization variety fn project_to_normalized(&self, rep: &mut DVector); - + // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never // been through a realization fn column_index(&self) -> Option; - + // assign the element a configuration matrix column index. this method must // be used carefully to preserve invariant (1), described in the comment on // the `tangent` field of the `Assembly` structure @@ -179,7 +179,7 @@ pub struct Sphere { impl Sphere { const CURVATURE_COMPONENT: usize = 3; - + pub fn new( id: String, label: String, @@ -203,7 +203,7 @@ impl Element for Sphere { fn default_id() -> String { "sphere".to_string() } - + fn default(id: String, id_num: u64) -> Self { Self::new( id, @@ -212,39 +212,39 @@ impl Element for Sphere { sphere(0.0, 0.0, 0.0, 1.0), ) } - + fn default_regulators(self: Rc) -> Vec> { vec![Rc::new(HalfCurvatureRegulator::new(self))] } - + fn id(&self) -> &String { &self.id } - + fn label(&self) -> &String { &self.label } - + fn representation(&self) -> Signal> { self.representation } - + fn ghost(&self) -> Signal { self.ghost } - + fn regulators(&self) -> Signal>> { self.regulators } - + fn project_to_normalized(&self, rep: &mut DVector) { project_sphere_to_normalized(rep); } - + fn column_index(&self) -> Option { self.column_index.get() } - + fn set_column_index(&self, index: usize) { self.column_index.set(Some(index)); } @@ -279,7 +279,7 @@ pub struct Point { impl Point { const WEIGHT_COMPONENT: usize = 3; const NORM_COMPONENT: usize = 4; - + pub fn new( id: String, label: String, @@ -303,7 +303,7 @@ impl Element for Point { fn default_id() -> String { "point".to_string() } - + fn default(id: String, id_num: u64) -> Self { Self::new( id, @@ -321,35 +321,35 @@ impl Element for Point { }) .collect() } - + fn id(&self) -> &String { &self.id } - + fn label(&self) -> &String { &self.label } - + fn representation(&self) -> Signal> { self.representation } - + fn ghost(&self) -> Signal { self.ghost } - + fn regulators(&self) -> Signal>> { self.regulators } - + fn project_to_normalized(&self, rep: &mut DVector) { project_point_to_normalized(rep); } - + fn column_index(&self) -> Option { self.column_index.get() } - + fn set_column_index(&self, index: usize) { self.column_index.set(Some(index)); } @@ -420,10 +420,10 @@ impl InversiveDistanceRegulator { ) ) }); - + let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - + Self { subjects, measurement, set_point, serial } } } @@ -432,11 +432,11 @@ impl Regulator for InversiveDistanceRegulator { fn subjects(&self) -> Vec> { self.subjects.clone().into() } - + fn measurement(&self) -> ReadSignal { self.measurement } - + fn set_point(&self) -> Signal { self.set_point } @@ -475,10 +475,10 @@ impl HalfCurvatureRegulator { let measurement = subject.representation().map( |rep| rep[Sphere::CURVATURE_COMPONENT] ); - + let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - + Self { subject, measurement, set_point, serial } } } @@ -487,11 +487,11 @@ impl Regulator for HalfCurvatureRegulator { fn subjects(&self) -> Vec> { vec![self.subject.clone()] } - + fn measurement(&self) -> ReadSignal { self.measurement } - + fn set_point(&self) -> Signal { self.set_point } @@ -600,7 +600,7 @@ pub struct Assembly { // elements and regulators pub elements: Signal>>, pub regulators: Signal>>, - + // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column // indices. when you realize the assembly, every element that's present @@ -612,13 +612,13 @@ pub struct Assembly { // in that column of the tangent space basis matrices // pub tangent: Signal, - + // indexing pub elements_by_id: Signal>>, - + // realization control pub realization_trigger: Signal<()>, - + // realization diagnostics pub realization_status: Signal>, pub descent_history: Signal, @@ -638,7 +638,7 @@ impl Assembly { descent_history: create_signal(DescentHistory::new()), step: create_signal(SpecifiedValue::from_empty_spec()), }; - + // realize the assembly whenever the element list, the regulator list, // a regulator's set point, or the realization trigger is updated let assembly_for_realization = assembly.clone(); @@ -652,7 +652,7 @@ impl Assembly { assembly_for_realization.realization_trigger.track(); assembly_for_realization.realize(); }); - + // load a configuration from the descent history whenever the active // step is updated let assembly_for_step_selection = assembly.clone(); @@ -664,12 +664,12 @@ impl Assembly { assembly_for_step_selection.load_config(&config) } }); - + assembly } - + // --- inserting elements and regulators --- - + // 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 @@ -679,13 +679,13 @@ impl Assembly { let elt_rc = Rc::new(elt); self.elements.update(|elts| elts.insert(elt_rc.clone())); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); - + // create and insert the element's default regulators for reg in elt_rc.default_regulators() { self.insert_regulator(reg); } } - + pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(elt.id()) @@ -695,7 +695,7 @@ impl Assembly { } can_insert } - + pub fn insert_element_default(&self) { // find the next unused identifier in the default sequence let default_id = T::default_id(); @@ -707,17 +707,17 @@ impl Assembly { id_num += 1; id = format!("{default_id}{id_num}"); } - + // create and insert the default example of `T` let _ = self.insert_element_unchecked(T::default(id, id_num)); } - + pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list self.regulators.update( |regs| regs.insert(regulator.clone()) ); - + // add the regulator to each subject's regulator list let subject_regulators: Vec<_> = regulator.subjects().into_iter().map( |subj| subj.regulators() @@ -725,7 +725,7 @@ impl Assembly { for regulators in subject_regulators { regulators.update(|regs| regs.insert(regulator.clone())); } - + /* DEBUG */ // print an updated list of regulators console_log!("Regulators:"); @@ -748,9 +748,9 @@ impl Assembly { } }); } - + // --- updating the configuration --- - + pub fn load_config(&self, config: &DMatrix) { for elt in self.elements.get_clone_untracked() { elt.representation().update( @@ -758,9 +758,9 @@ impl Assembly { ); } } - + // --- realization --- - + pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { @@ -768,7 +768,7 @@ impl Assembly { elt.set_column_index(index); } }); - + // set up the constraint problem let problem = self.elements.with_untracked(|elts| { let mut problem = ConstraintProblem::new(elts.len()); @@ -782,21 +782,21 @@ impl Assembly { }); problem }); - + /* DEBUG */ // log the Gram matrix console_log!("Gram matrix:\n{}", problem.gram); console_log!("Frozen entries:\n{}", problem.frozen); - + /* DEBUG */ // log the initial configuration matrix console_log!("Old configuration:{:>8.3}", problem.guess); - + // look for a configuration with the given Gram matrix let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - + /* DEBUG */ // report the outcome of the search in the browser console if let Err(ref message) = result { @@ -808,20 +808,20 @@ impl Assembly { console_log!("Steps: {}", history.scaled_loss.len() - 1); console_log!("Loss: {}", history.scaled_loss.last().unwrap()); } - + // report the descent history let step_cnt = history.config.len(); self.descent_history.set(history); - + match result { Ok(ConfigNeighborhood { nbhd: tangent, .. }) => { /* DEBUG */ // report the tangent dimension console_log!("Tangent dimension: {}", tangent.dim()); - + // report the realization status self.realization_status.set(Ok(())); - + // display the last realization step self.step.set( if step_cnt > 0 { @@ -831,7 +831,7 @@ impl Assembly { SpecifiedValue::from_empty_spec() } ); - + // save the tangent space self.tangent.set_silent(tangent); }, @@ -841,15 +841,15 @@ impl Assembly { // `Err(message)` we received from the match: we're changing the // `Ok` type from `Realization` to `()` self.realization_status.set(Err(message)); - + // display the initial guess self.step.set(SpecifiedValue::from(Some(0.0))); }, } } - + // --- deformation --- - + // project the given motion to the tangent space of the solution variety and // move the assembly along it. the implementation is based on invariant (1) // from above and the following additional invariant: @@ -866,7 +866,7 @@ impl Assembly { if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) { console::log_1(&JsValue::from("The assembly is rigid")); } - + // give a column index to each moving element that doesn't have one yet. // this temporarily breaks invariant (1), but the invariant will be // restored when we realize the assembly at the end of the deformation. @@ -884,7 +884,7 @@ impl Assembly { } next_column_index }; - + // project the element motions onto the tangent space of the solution // variety and sum them to get a deformation of the whole assembly. the // matrix `motion_proj` that holds the deformation has extra columns for @@ -895,7 +895,7 @@ impl Assembly { // we can unwrap the column index because we know that every moving // element has one at this point let column_index = elt_motion.element.column_index().unwrap(); - + if column_index < realized_dim { // this element had a column index when we started, so by // invariant (1), it's reflected in the tangent space @@ -913,7 +913,7 @@ impl Assembly { target_column += unif_to_std * elt_motion.velocity; } } - + // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward for elt in self.elements.get_clone_untracked() { @@ -931,7 +931,7 @@ impl Assembly { }; }); } - + // trigger a realization to bring the configuration back onto the // solution variety. this also gets the elements' column indices and the // saved tangent space back in sync @@ -942,9 +942,9 @@ impl Assembly { #[cfg(test)] mod tests { use super::*; - + use crate::engine; - + #[test] #[should_panic(expected = "Sphere \"sphere\" must be indexed before it writes problem data")] @@ -954,7 +954,7 @@ mod tests { elt.pose(&mut ConstraintProblem::new(1)); }); } - + #[test] #[should_panic(expected = "Subject \"sphere1\" must be indexed before \ inversive distance regulator writes problem data")] @@ -972,7 +972,7 @@ mod tests { }.pose(&mut ConstraintProblem::new(2)); }); } - + #[test] fn curvature_drift_test() { const INITIAL_RADIUS: f64 = 0.25; @@ -992,7 +992,7 @@ mod tests { engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS), ) ); - + // nudge the sphere repeatedly along the `z` axis const STEP_SIZE: f64 = 0.0025; const STEP_CNT: usize = 400; @@ -1008,7 +1008,7 @@ mod tests { ] ); } - + // check how much the sphere's curvature has drifted const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; const DRIFT_TOL: f64 = 0.015; diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index 51d58f1..0909f45 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -54,7 +54,7 @@ fn StepInput() -> View { // get the assembly let state = use_context::(); let assembly = state.assembly; - + // the `last_step` signal holds the index of the last step let last_step = assembly.descent_history.map( |history| match history.config.len() { @@ -63,15 +63,15 @@ fn StepInput() -> View { } ); let input_max = last_step.map(|last| last.unwrap_or(0)); - + // these signals hold the entered step number let value = create_signal(String::new()); let value_as_number = create_signal(0.0); - + create_effect(move || { value.set(assembly.step.with(|n| n.spec.clone())); }); - + view! { div(id = "step-input") { label { "Step" } @@ -98,7 +98,7 @@ fn StepInput() -> View { |val| val.clamp(0.0, input_max.get() as f64) ) ); - + // set the input string and the assembly's active step value.set(step.spec.clone()); assembly.step.set(step); @@ -124,7 +124,7 @@ fn LossHistory() -> View { const CONTAINER_ID: &str = "loss-history"; let state = use_context::(); let renderer = WasmRenderer::new_opt(None, Some(178)); - + on_mount(move || { create_effect(move || { // get the loss history @@ -136,13 +136,13 @@ fn LossHistory() -> View { .map(into_log10_time_point) .collect() ); - + // initialize the chart axes let step_axis = Axis::new() .type_(AxisType::Category) .boundary_gap(false); let scaled_loss_axis = Axis::new(); - + // load the chart data. when there's no history, we load the data // point (0, None) to clear the chart. it would feel more natural to // load empty data vectors, but that turns out not to clear the @@ -164,7 +164,7 @@ fn LossHistory() -> View { renderer.render(CONTAINER_ID, &chart).unwrap(); }); }); - + view! { div(id = CONTAINER_ID, class = "diagnostics-chart") } @@ -176,7 +176,7 @@ fn SpectrumHistory() -> View { const CONTAINER_ID: &str = "spectrum-history"; let state = use_context::(); let renderer = WasmRenderer::new(478, 178); - + on_mount(move || { create_effect(move || { // get the spectrum of the Hessian at each step, split into its @@ -208,13 +208,13 @@ fn SpectrumHistory() -> View { ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero .into_iter() .partition(|&(_, val)| val > 0.0); - + // initialize the chart axes let step_axis = Axis::new() .type_(AxisType::Category) .boundary_gap(false); let eigval_axis = Axis::new(); - + // load the chart data. when there's no history, we load the data // point (0, None) to clear the chart. it would feel more natural to // load empty data vectors, but that turns out not to clear the @@ -270,7 +270,7 @@ fn SpectrumHistory() -> View { renderer.render(CONTAINER_ID, &chart).unwrap(); }); }); - + view! { div(id = CONTAINER_ID, class = "diagnostics-chart") } @@ -302,7 +302,7 @@ pub fn Diagnostics() -> View { let diagnostics_state = DiagnosticsState::new("loss".to_string()); let active_tab = diagnostics_state.active_tab.clone(); provide_context(diagnostics_state); - + view! { div(id = "diagnostics") { div(id = "diagnostics-bar") { @@ -317,4 +317,4 @@ pub fn Diagnostics() -> View { DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } } } -} \ No newline at end of file +} diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index 98be85e..07882c5 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -48,11 +48,11 @@ impl SceneSpheres { highlights: Vec::new(), } } - + fn len_i32(&self) -> i32 { self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") } - + fn push( &mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, @@ -79,7 +79,7 @@ impl ScenePoints { selections: Vec::new(), } } - + fn push( &mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool, @@ -107,7 +107,7 @@ impl Scene { pub trait DisplayItem { fn show(&self, scene: &mut Scene, selected: bool); - + // the smallest positive depth, represented as a multiple of `dir`, where // the line generated by `dir` hits the element. returns `None` if the line // misses the element @@ -125,14 +125,14 @@ impl DisplayItem for Sphere { const DEFAULT_OPACITY: f32 = 0.5; const GHOST_OPACITY: f32 = 0.2; const HIGHLIGHT: f32 = 0.2; - + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; scene.spheres.push(representation, color, opacity, highlight); } - + // this method should be kept synchronized with `sphere_cast` in // `spheres.frag`, which does essentially the same thing on the GPU side fn cast( @@ -144,12 +144,12 @@ impl DisplayItem for Sphere { // if `a/b` is less than this threshold, we approximate // `a*u^2 + b*u + c` by the linear function `b*u + c` const DEG_THRESHOLD: f64 = 1e-9; - + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); let a = -rep[3] * dir.norm_squared(); let b = rep.rows_range(..3).dot(&dir); let c = -rep[4]; - + let adjust = 4.0*a*c/(b*b); if adjust < 1.0 { // as long as `b` is non-zero, the linear approximation of @@ -184,14 +184,14 @@ impl DisplayItem for Point { /* SCAFFOLDING */ const GHOST_OPACITY: f32 = 0.4; const HIGHLIGHT: f32 = 0.5; - + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; scene.points.push(representation, color, opacity, highlight, selected); } - + /* SCAFFOLDING */ fn cast( &self, @@ -203,16 +203,16 @@ impl DisplayItem for Point { if rep[2] < 0.0 { // this constant should be kept synchronized with `point.frag` const POINT_RADIUS_PX: f64 = 4.0; - + // find the radius of the point in screen projection units let point_radius_proj = POINT_RADIUS_PX * pixel_size; - + // find the squared distance between the screen projections of the // ray and the point let dir_proj = -dir.fixed_rows::<2>(0) / dir[2]; let rep_proj = -rep.fixed_rows::<2>(0) / rep[2]; let dist_sq = (dir_proj - rep_proj).norm_squared(); - + // if the ray hits the point, return its depth if dist_sq < point_radius_proj * point_radius_proj { Some(rep[2] / dir[2]) @@ -254,13 +254,13 @@ fn set_up_program( WebGl2RenderingContext::FRAGMENT_SHADER, fragment_shader_source, ); - + // create the program and attach the shaders let program = context.create_program().unwrap(); context.attach_shader(&program, &vertex_shader); context.attach_shader(&program, &fragment_shader); context.link_program(&program); - + /* DEBUG */ // report whether linking succeeded let link_status = context @@ -273,7 +273,7 @@ fn set_up_program( "Linking failed" }; console::log_1(&JsValue::from(link_msg)); - + program } @@ -318,7 +318,7 @@ fn load_new_buffer( // create a buffer and bind it to ARRAY_BUFFER let buffer = context.create_buffer(); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); - + // load the given data into the buffer. this block is unsafe because // `Float32Array::view` creates a raw view into our module's // `WebAssembly.Memory` buffer. allocating more memory will change the @@ -332,7 +332,7 @@ fn load_new_buffer( WebGl2RenderingContext::STATIC_DRAW, ); } - + buffer } @@ -353,11 +353,11 @@ fn event_dir(event: &MouseEvent) -> (Vector3, f64) { let width = rect.width(); let height = rect.height(); let shortdim = width.min(height); - + // this constant should be kept synchronized with `spheres.frag` and // `point.vert` const FOCAL_SLOPE: f64 = 0.3; - + ( Vector3::new( FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, @@ -373,13 +373,13 @@ fn event_dir(event: &MouseEvent) -> (Vector3, f64) { #[component] pub fn Display() -> View { let state = use_context::(); - + // canvas let display = create_node_ref(); - + // viewpoint let assembly_to_world = create_signal(DMatrix::::identity(5, 5)); - + // navigation let pitch_up = create_signal(0.0); let pitch_down = create_signal(0.0); @@ -390,7 +390,7 @@ pub fn Display() -> View { let zoom_in = create_signal(0.0); let zoom_out = create_signal(0.0); let turntable = create_signal(false); /* BENCHMARKING */ - + // manipulation let translate_neg_x = create_signal(0.0); let translate_pos_x = create_signal(0.0); @@ -400,7 +400,7 @@ pub fn Display() -> View { let translate_pos_z = create_signal(0.0); let shrink_neg = create_signal(0.0); let shrink_pos = create_signal(0.0); - + // change listener let scene_changed = create_signal(true); create_effect(move || { @@ -413,18 +413,18 @@ pub fn Display() -> View { state.selection.track(); scene_changed.set(true); }); - + /* INSTRUMENTS */ const SAMPLE_PERIOD: i32 = 60; let mut last_sample_time = 0.0; let mut frames_since_last_sample = 0; let mean_frame_interval = create_signal(0.0); - + let assembly_for_raf = state.assembly.clone(); on_mount(move || { // timing let mut last_time = 0.0; - + // viewpoint const ROT_SPEED: f64 = 0.4; // in radians per second const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second @@ -432,18 +432,18 @@ pub fn Display() -> View { let mut orientation = DMatrix::::identity(5, 5); let mut rotation = DMatrix::::identity(5, 5); let mut location_z: f64 = 5.0; - + // manipulation const TRANSLATION_SPEED: f64 = 0.15; // in length units per second const SHRINKING_SPEED: f64 = 0.15; // in length units per second - + // display parameters const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ - + /* INSTRUMENTS */ let performance = window().unwrap().performance().unwrap(); - + // get the display canvas let canvas = display.get().unchecked_into::(); let ctx = canvas @@ -452,28 +452,28 @@ pub fn Display() -> View { .unwrap() .dyn_into::() .unwrap(); - + // disable depth testing ctx.disable(WebGl2RenderingContext::DEPTH_TEST); - + // set blend mode ctx.enable(WebGl2RenderingContext::BLEND); ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA); - + // set up the sphere rendering program let sphere_program = set_up_program( &ctx, include_str!("identity.vert"), include_str!("spheres.frag"), ); - + // set up the point rendering program let point_program = set_up_program( &ctx, include_str!("point.vert"), include_str!("point.frag"), ); - + /* DEBUG */ // print the maximum number of vectors that can be passed as // uniforms to a fragment shader. the OpenGL ES 3.0 standard @@ -490,10 +490,10 @@ pub fn Display() -> View { &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), &JsValue::from("uniform vectors available"), ); - + // find the sphere program's vertex attribute let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32; - + // find the sphere program's uniforms const SPHERE_MAX: usize = 200; let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt"); @@ -513,7 +513,7 @@ pub fn Display() -> View { let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); - + // load the viewport vertex positions into a new vertex buffer object const VERTEX_CNT: usize = 6; let viewport_positions: [f32; 3*VERTEX_CNT] = [ @@ -527,20 +527,20 @@ pub fn Display() -> View { 1.0, -1.0, 0.0, ]; let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); - + // find the point program's vertex attributes let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32; let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32; let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32; let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32; - + // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { // get the time step let time = performance.now(); let time_step = 0.001*(time - last_time); last_time = time; - + // get the navigation state let pitch_up_val = pitch_up.get(); let pitch_down_val = pitch_down.get(); @@ -551,7 +551,7 @@ pub fn Display() -> View { let zoom_in_val = zoom_in.get(); let zoom_out_val = zoom_out.get(); let turntable_val = turntable.get(); /* BENCHMARKING */ - + // get the manipulation state let translate_neg_x_val = translate_neg_x.get(); let translate_pos_x_val = translate_pos_x.get(); @@ -561,7 +561,7 @@ pub fn Display() -> View { let translate_pos_z_val = translate_pos_z.get(); let shrink_neg_val = shrink_neg.get(); let shrink_pos_val = shrink_pos.get(); - + // update the assembly's orientation let ang_vel = { let pitch = pitch_up_val - pitch_down_val; @@ -582,11 +582,11 @@ pub fn Display() -> View { Rotation3::from_scaled_axis(time_step * ang_vel).matrix() ); orientation = &rotation * &orientation; - + // update the assembly's location let zoom = zoom_out_val - zoom_in_val; location_z *= (time_step * ZOOM_SPEED * zoom).exp(); - + // manipulate the assembly /* KLUDGE */ // to avoid the complexity of making tangent space projection @@ -642,11 +642,11 @@ pub fn Display() -> View { scene_changed.set(true); } } - + if scene_changed.get() { const SPACE_DIM: usize = 3; const COLOR_SIZE: usize = 3; - + /* INSTRUMENTS */ // measure mean frame interval frames_since_last_sample += 1; @@ -655,11 +655,11 @@ pub fn Display() -> View { last_sample_time = time; frames_since_last_sample = 0; } - + // --- get the assembly --- - + let mut scene = Scene::new(); - + // find the map from assembly space to world space let location = { let u = -location_z; @@ -672,7 +672,7 @@ pub fn Display() -> View { ]) }; let asm_to_world = &location * &orientation; - + // set up the scene state.assembly.elements.with_untracked( |elts| for elt in elts { @@ -681,26 +681,26 @@ pub fn Display() -> View { } ); let sphere_cnt = scene.spheres.len_i32(); - + // --- draw the spheres --- - + // use the sphere rendering program ctx.use_program(Some(&sphere_program)); - + // enable the sphere program's vertex attribute ctx.enable_vertex_attrib_array(viewport_position_attr); - + // write the spheres in world coordinates let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( |rep| (&asm_to_world * rep).cast::() ).collect(); - + // set the resolution let width = canvas.width() as f32; let height = canvas.height() as f32; ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); - + // pass the scene data ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt); for n in 0..sphere_reps_world.len() { @@ -722,33 +722,33 @@ pub fn Display() -> View { scene.spheres.highlights[n], ); } - + // pass the display parameters ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); - + // bind the viewport vertex position buffer to the position // attribute in the vertex shader bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer); - + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); - + // disable the sphere program's vertex attribute ctx.disable_vertex_attrib_array(viewport_position_attr); - + // --- draw the points --- - + if !scene.points.representations.is_empty() { // use the point rendering program ctx.use_program(Some(&point_program)); - + // enable the point program's vertex attributes ctx.enable_vertex_attrib_array(point_position_attr); ctx.enable_vertex_attrib_array(point_color_attr); ctx.enable_vertex_attrib_array(point_highlight_attr); ctx.enable_vertex_attrib_array(point_selection_attr); - + // write the points in world coordinates let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); let point_positions = DMatrix::from_columns( @@ -756,7 +756,7 @@ pub fn Display() -> View { |rep| &asm_to_world_sp * rep ).collect::>().as_slice() ).cast::(); - + // load the point positions and colors into new buffers and // bind them to the corresponding attributes in the vertex // shader @@ -764,22 +764,22 @@ pub fn Display() -> View { bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); - + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); - + // disable the point program's vertex attributes ctx.disable_vertex_attrib_array(point_position_attr); ctx.disable_vertex_attrib_array(point_color_attr); ctx.disable_vertex_attrib_array(point_highlight_attr); ctx.disable_vertex_attrib_array(point_selection_attr); } - + // --- update the display state --- - + // update the viewpoint assembly_to_world.set(asm_to_world); - + // clear the scene change flag scene_changed.set( pitch_up_val != 0.0 @@ -799,7 +799,7 @@ pub fn Display() -> View { }); start_animation_loop(); }); - + let set_nav_signal = move |event: &KeyboardEvent, value: f64| { let mut navigating = true; let shift = event.shift_key(); @@ -819,7 +819,7 @@ pub fn Display() -> View { event.prevent_default(); } }; - + let set_manip_signal = move |event: &KeyboardEvent, value: f64| { let mut manipulating = true; let shift = event.shift_key(); @@ -838,7 +838,7 @@ pub fn Display() -> View { event.prevent_default(); } }; - + view! { /* TO DO */ // switch back to integer-valued parameters when that becomes possible @@ -860,7 +860,7 @@ pub fn Display() -> View { yaw_left.set(0.0); pitch_up.set(0.0); pitch_down.set(0.0); - + // swap manipulation inputs translate_pos_z.set(translate_neg_y.get()); translate_neg_z.set(translate_pos_y.get()); @@ -886,7 +886,7 @@ pub fn Display() -> View { roll_ccw.set(0.0); zoom_in.set(0.0); zoom_out.set(0.0); - + // swap manipulation inputs translate_pos_y.set(translate_neg_z.get()); translate_neg_y.set(translate_pos_z.get()); @@ -927,7 +927,7 @@ pub fn Display() -> View { None => (), }; } - + // if we clicked something, select it match clicked { Some((elt, _)) => state.select(&elt, event.shift_key()), @@ -936,4 +936,4 @@ pub fn Display() -> View { }, ) } -} \ No newline at end of file +} diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs index 547b73b..fc041c7 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/components/outline.rs @@ -21,16 +21,16 @@ 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( set_point.with_untracked(|set_pt| set_pt.spec.clone()) ); - + // this `reset_value` closure resets the input value to the regulator's set // point specification let reset_value = move || { @@ -39,11 +39,11 @@ fn RegulatorInput(regulator: Rc) -> View { value.set(set_point.with(|set_pt| set_pt.spec.clone())); }) }; - + // reset the input value whenever the regulator's set point specification // is updated create_effect(reset_value); - + view! { input( r#type = "text", @@ -241,7 +241,7 @@ fn ElementOutlineItem(element: Rc) -> View { #[component] pub fn Outline() -> View { let state = use_context::(); - + // list the elements alphabetically by ID /* TO DO */ // this code is designed to generalize easily to other sort keys. if we only @@ -254,7 +254,7 @@ pub fn Outline() -> View { .sorted_by_key(|elt| elt.id().clone()) .collect::>() ); - + view! { ul( id = "outline", @@ -272,4 +272,4 @@ pub fn Outline() -> View { ) } } -} \ No newline at end of file +} diff --git a/app-proto/src/components/point.frag b/app-proto/src/components/point.frag index 194a072..5e18bb4 100644 --- a/app-proto/src/components/point.frag +++ b/app-proto/src/components/point.frag @@ -10,10 +10,10 @@ out vec4 outColor; void main() { float r = total_radius * length(2.*gl_PointCoord - vec2(1.)); - + const float POINT_RADIUS = 4.; float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); float disk = 1. - smoothstep(total_radius - 1., total_radius, r); vec4 color = mix(point_color, vec4(1.), border * point_highlight); outColor = vec4(vec3(1.), disk) * color; -} \ No newline at end of file +} diff --git a/app-proto/src/components/point.vert b/app-proto/src/components/point.vert index 0b76bc1..14eb2e7 100644 --- a/app-proto/src/components/point.vert +++ b/app-proto/src/components/point.vert @@ -14,11 +14,11 @@ const float focal_slope = 0.3; void main() { total_radius = 5. + 0.5*selected; - + float depth = -focal_slope * position.z; gl_Position = vec4(position.xy / depth, 0., 1.); gl_PointSize = 2.*total_radius; - + point_color = color; point_highlight = highlight; -} \ No newline at end of file +} diff --git a/app-proto/src/components/spheres.frag b/app-proto/src/components/spheres.frag index fa317a8..172109a 100644 --- a/app-proto/src/components/spheres.frag +++ b/app-proto/src/components/spheres.frag @@ -75,7 +75,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) { // point. i calculated it in my head and decided that the result looked good // enough for now vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); - + float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a)); @@ -110,7 +110,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) { float a = -v.lt.s * dot(dir, dir); float b = dot(v.sp, dir); float c = -v.lt.t; - + float adjust = 4.*a*c/(b*b); if (adjust < 1.) { // as long as `b` is non-zero, the linear approximation of @@ -136,7 +136,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) { void main() { vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; vec3 dir = vec3(focal_slope * scr, -1.); - + // cast rays through the spheres const int LAYER_MAX = 12; TaggedDepth top_hits [LAYER_MAX]; @@ -144,7 +144,7 @@ void main() { for (int id = 0; id < sphere_cnt; ++id) { // find out where the ray hits the sphere vec2 hit_depths = sphere_cast(sphere_list[id], dir); - + // insertion-sort the points we hit into the hit list float dimming = 1.; for (int side = 0; side < 2; ++side) { @@ -169,14 +169,14 @@ void main() { } } } - + /* DEBUG */ // in debug mode, show the layer count instead of the shaded image if (debug_mode) { // at the bottom of the screen, show the color scale instead of the // layer count if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x); - + // convert number to color ivec3 bits = layer_cnt / ivec3(1, 2, 4); vec3 color = mod(vec3(bits), 2.); @@ -186,7 +186,7 @@ void main() { outColor = vec4(color, 1.); return; } - + // composite the sphere fragments vec3 color = vec3(0.); int layer = layer_cnt - 1; @@ -203,7 +203,7 @@ void main() { // load the current fragment Fragment frag = frag_next; float highlight = highlight_next; - + // shade the next fragment hit = top_hits[layer]; sphere_color = color_list[hit.id]; @@ -213,23 +213,23 @@ void main() { vec4(hit.dimming * sphere_color.rgb, sphere_color.a) ); highlight_next = highlight_list[hit.id]; - + // highlight intersections float ixn_dist = intersection_dist(frag, frag_next); float max_highlight = max(highlight, highlight_next); float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); frag.color = mix(frag.color, vec4(1.), ixn_highlight); frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight); - + // highlight cusps float cusp_cos = abs(dot(dir, frag.normal)); float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s); float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); frag.color = mix(frag.color, vec4(1.), cusp_highlight); - + // composite the current fragment color = mix(color, frag.color.rgb, frag.color.a); } color = mix(color, frag_next.color.rgb, frag_next.color.a); outColor = vec4(sRGB(color), 1.); -} \ No newline at end of file +} diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index 0d387d3..d572362 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -144,7 +144,7 @@ fn load_low_curvature(assembly: &Assembly) { engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), ) ); - + // impose the desired tangencies and make the sides planar let index_range = 1..=3; let [central, assemb_plane] = ["central", "assemb_plane"].map( @@ -217,7 +217,7 @@ fn load_pointed(assembly: &Assembly) { for index_y in 0..=1 { let x = index_x as f64 - 0.5; let y = index_y as f64 - 0.5; - + let _ = assembly.try_insert_element( Sphere::new( format!("sphere{index_x}{index_y}"), @@ -226,7 +226,7 @@ fn load_pointed(assembly: &Assembly) { engine::sphere(x, y, 0.0, 1.0), ) ); - + let _ = assembly.try_insert_element( Point::new( format!("point{index_x}{index_y}"), @@ -310,7 +310,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) { for vertex in vertices { let _ = assembly.try_insert_element(vertex); } - + // create the faces const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt(); @@ -339,7 +339,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) { face.ghost().set(true); let _ = assembly.try_insert_element(face); } - + let index_range = 1..=3; for j in index_range.clone() { // make each face planar @@ -352,7 +352,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) { curvature_regulator.set_point().set( SpecifiedValue::try_from("0".to_string()).unwrap() ); - + // put each A vertex on the face it belongs to let vertex_a = assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id[&format!("a{j}")].clone() @@ -360,7 +360,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) { let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]); incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); assembly.insert_regulator(Rc::new(incidence_a)); - + // regulate the B-C vertex distances let vertices_bc = ["b", "c"].map( |series| assembly.elements_by_id.with_untracked( @@ -370,10 +370,10 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) { assembly.insert_regulator( Rc::new(InversiveDistanceRegulator::new(vertices_bc)) ); - + // get the pair of indices adjacent to `j` let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1]; - + for k in adjacent_indices.clone() { for series in ["b", "c"] { // put each B and C vertex on the faces it belongs to @@ -383,14 +383,14 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) { let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]); incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); assembly.insert_regulator(Rc::new(incidence)); - + // regulate the A-B and A-C vertex distances assembly.insert_regulator( Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex])) ); } } - + // regulate the A-A and C-C vertex distances let adjacent_pairs = ["a", "c"].map( |series| adjacent_indices.map( @@ -422,14 +422,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) { let substrate = assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id["substrate"].clone() ); - + // fix the substrate's curvature substrate.regulators().with_untracked( |regs| regs.first().unwrap().clone() ).set_point().set( SpecifiedValue::try_from("0.5".to_string()).unwrap() ); - + // add the circles to be packed const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32]; const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32]; @@ -445,10 +445,10 @@ fn load_dodecahedral_packing(assembly: &Assembly) { for k in 0..2 { let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0); let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi; - + let id_num = format!("{j}{k}"); let label_sub = format!("{}{}", subscripts[j], subscripts[k]); - + // add the A face let id_a = format!("a{id_num}"); let _ = assembly.try_insert_element( @@ -464,7 +464,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) { |elts_by_id| elts_by_id[&id_a].clone() ) ); - + // add the B face let id_b = format!("b{id_num}"); let _ = assembly.try_insert_element( @@ -480,7 +480,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) { |elts_by_id| elts_by_id[&id_b].clone() ) ); - + // add the C face let id_c = format!("c{id_num}"); let _ = assembly.try_insert_element( @@ -498,14 +498,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) { ); } } - + // make each face sphere perpendicular to the substrate for face in faces { let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]); right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); assembly.insert_regulator(Rc::new(right_angle)); } - + // set up the tangencies that define the packing for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] { for k in 0..2 { @@ -524,14 +524,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) { ) ) ); - + // set up the short-edge tangency let short_tangency = InversiveDistanceRegulator::new(short_edge.clone()); if k == 0 { short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); } assembly.insert_regulator(Rc::new(short_tangency)); - + // set up the side tangencies for i in 0..2 { for j in 0..2 { @@ -577,14 +577,14 @@ fn load_balanced(assembly: &Assembly) { for sphere in spheres { let _ = assembly.try_insert_element(sphere); } - + // get references to the spheres let [outer, a, b] = ["outer", "a", "b"].map( |id| assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id[id].clone() ) ); - + // fix the diameters of the outer, sun, and moon spheres for (sphere, radius) in [ (outer.clone(), R_OUTER), @@ -599,7 +599,7 @@ fn load_balanced(assembly: &Assembly) { SpecifiedValue::try_from(curvature.to_string()).unwrap() ); } - + // set the inversive distances between the spheres. as described above, the // initial configuration deliberately violates these constraints for inner in [a, b] { @@ -629,14 +629,14 @@ fn load_off_center(assembly: &Assembly) { engine::sphere(0.0, 0.0, 0.0, 1.0), ), ); - + // get references to the elements let point_and_sphere = ["point", "sphere"].map( |id| assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id[id].clone() ) ); - + // put the point on the sphere let incidence = InversiveDistanceRegulator::new(point_and_sphere); incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); @@ -650,7 +650,7 @@ fn load_off_center(assembly: &Assembly) { // inversive distance of 0 between the circumsphere and each vertex fn load_radius_ratio(assembly: &Assembly) { let index_range = 1..=4; - + // create the spheres const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; let spheres = [ @@ -670,7 +670,7 @@ fn load_radius_ratio(assembly: &Assembly) { for sphere in spheres { let _ = assembly.try_insert_element(sphere); } - + // create the vertices let vertices = izip!( index_range.clone(), @@ -699,7 +699,7 @@ fn load_radius_ratio(assembly: &Assembly) { for vertex in vertices { let _ = assembly.try_insert_element(vertex); } - + // create the faces let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize(); let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6)); @@ -731,7 +731,7 @@ fn load_radius_ratio(assembly: &Assembly) { face.ghost().set(true); let _ = assembly.try_insert_element(face); } - + // impose the constraints for j in index_range.clone() { let [face_j, vertex_j] = [ @@ -742,7 +742,7 @@ fn load_radius_ratio(assembly: &Assembly) { |elts_by_id| elts_by_id[&id].clone() ) ); - + // make the faces planar let curvature_regulator = face_j.regulators().with_untracked( |regs| regs.first().unwrap().clone() @@ -750,12 +750,12 @@ fn load_radius_ratio(assembly: &Assembly) { curvature_regulator.set_point().set( SpecifiedValue::try_from("0".to_string()).unwrap() ); - + for k in index_range.clone().filter(|&index| index != j) { let vertex_k = assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id[&format!("v{k}")].clone() ); - + // fix the distances between the vertices if j < k { let distance_regulator = InversiveDistanceRegulator::new( @@ -763,7 +763,7 @@ fn load_radius_ratio(assembly: &Assembly) { ); assembly.insert_regulator(Rc::new(distance_regulator)); } - + // put the vertices on the faces let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]); incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); @@ -799,7 +799,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) { [0.00_f32, 0.25_f32, 1.00_f32], [0.25_f32, 0.00_f32, 1.00_f32], ].into_iter(); - + // create the spheres let spheres = [ Sphere::new( @@ -836,7 +836,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) { for sphere in spheres { let _ = assembly.try_insert_element(sphere); } - + // put the outer sphere in ghost mode and fix its curvature let outer = assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id["outer"].clone() @@ -848,7 +848,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) { outer_curvature_regulator.set_point().set( SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap() ); - + // impose the desired tangencies let [outer, sun, moon] = ["outer", "sun", "moon"].map( |id| assembly.elements_by_id.with_untracked( @@ -872,11 +872,11 @@ fn load_irisawa_hexlet(assembly: &Assembly) { assembly.insert_regulator(Rc::new(tangency)); } } - + let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]); outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); assembly.insert_regulator(Rc::new(outer_sun_tangency)); - + let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]); outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); assembly.insert_regulator(Rc::new(outer_moon_tangency)); @@ -895,18 +895,18 @@ pub fn TestAssemblyChooser() -> View { console::log_1( &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) ); - + batch(|| { let state = use_context::(); 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()); assembly.descent_history.set(DescentHistory::new()); state.selection.update(|sel| sel.clear()); - + // load assembly match name.as_str() { "general" => load_general(assembly), @@ -922,7 +922,7 @@ pub fn TestAssemblyChooser() -> View { }; }); }); - + // build the chooser view! { select(bind:value = assembly_name) { @@ -938,4 +938,4 @@ pub fn TestAssemblyChooser() -> View { option(value = "empty") { "Empty" } } } -} \ No newline at end of file +} diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 0f26f02..35265e5 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -62,19 +62,19 @@ impl PartialMatrix { pub fn new() -> Self { Self(Vec::::new()) } - + pub fn push(&mut self, row: usize, col: usize, value: f64) { let Self(entries) = self; entries.push(MatrixEntry { index: (row, col), value }); } - + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { self.push(row, col, value); if row != col { self.push(col, row, value); } } - + fn freeze(&self, a: &DMatrix) -> DMatrix { let mut result = a.clone(); for &MatrixEntry { index, value } in self { @@ -82,7 +82,7 @@ impl PartialMatrix { } result } - + fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); for &MatrixEntry { index, .. } in self { @@ -90,7 +90,7 @@ impl PartialMatrix { } result } - + fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); for &MatrixEntry { index, value } in self { @@ -112,7 +112,7 @@ impl Display for PartialMatrix { impl IntoIterator for PartialMatrix { type Item = MatrixEntry; type IntoIter = std::vec::IntoIter; - + fn into_iter(self) -> Self::IntoIter { let Self(entries) = self; entries.into_iter() @@ -122,7 +122,7 @@ impl IntoIterator for PartialMatrix { 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() @@ -146,7 +146,7 @@ impl ConfigSubspace { basis_std: Vec::new(), } } - + // approximate the kernel of a symmetric endomorphism of the configuration // space for `assembly_dim` elements. we consider an eigenvector to be part // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` @@ -167,10 +167,10 @@ impl ConfigSubspace { |(λ, v)| (λ.abs() < THRESHOLD).then_some(v) ).collect::>().as_slice() ); - + // express the basis in the standard coordinates let basis_std = proj_to_std * &basis_proj; - + const ELEMENT_DIM: usize = 5; const UNIFORM_DIM: usize = 4; Self { @@ -187,15 +187,15 @@ impl ConfigSubspace { ).collect(), } } - + pub fn dim(&self) -> usize { self.basis_std.len() } - + pub fn assembly_dim(&self) -> usize { self.assembly_dim } - + // find the projection onto this subspace of the motion where the element // with the given column index has velocity `v`. the velocity is given in // projection coordinates, and the projection is done with respect to the @@ -253,7 +253,7 @@ impl ConstraintProblem { guess: DMatrix::::zeros(ELEMENT_DIM, element_count), } } - + #[cfg(feature = "dev")] pub fn from_guess(guess_columns: &[DVector]) -> Self { Self { @@ -377,10 +377,10 @@ pub fn realize_gram( ) -> Realization { // destructure the problem data let ConstraintProblem { gram, guess, frozen } = problem; - + // start the descent history let mut history = DescentHistory::new(); - + // handle the case where the assembly is empty. our general realization // routine can't handle this case because it builds the Hessian using // `DMatrix::from_columns`, which panics when the list of columns is empty @@ -394,20 +394,20 @@ pub fn realize_gram( ); return Realization { result, history }; } - + // find the dimension of the search space let element_dim = guess.nrows(); let total_dim = element_dim * assembly_dim; - + // scale the tolerance let scale_adjustment = (gram.0.len() as f64).sqrt(); let tol = scale_adjustment * scaled_tol; - + // convert the frozen indices to stacked format let frozen_stacked: Vec = frozen.into_iter().map( |MatrixEntry { index: (row, col), .. }| col*element_dim + row ).collect(); - + // 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); @@ -416,7 +416,7 @@ pub fn realize_gram( let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj; let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>); history.neg_grad.push(neg_grad.clone()); - + // find the negative Hessian of the loss function let mut hess_cols = Vec::>::with_capacity(total_dim); for col in 0..assembly_dim { @@ -435,7 +435,7 @@ pub fn realize_gram( } } hess = DMatrix::from_columns(hess_cols.as_slice()); - + // regularize the Hessian let hess_eigvals = hess.symmetric_eigenvalues(); let min_eigval = hess_eigvals.min(); @@ -443,7 +443,7 @@ pub fn realize_gram( hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } history.hess_eigvals.push(hess_eigvals); - + // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace let zero_col = DVector::zeros(total_dim); @@ -454,12 +454,12 @@ pub fn realize_gram( hess.set_column(k, &zero_col); hess[(k, k)] = 1.0; } - + // stop if the loss is tolerably low history.config.push(state.config.clone()); history.scaled_loss.push(state.loss / scale_adjustment); if state.loss < tol { break; } - + // compute the Newton step /* TO DO */ /* @@ -479,7 +479,7 @@ pub fn realize_gram( let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); - + // use backtracking line search to find a better configuration if let Some((better_state, backoff_steps)) = seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), @@ -505,10 +505,10 @@ pub fn realize_gram( .view_mut(block_start, (element_dim, UNIFORM_DIM)) .copy_from(&local_unif_to_std(state.config.column(n))); } - + // find the kernel of the Hessian. give it the uniform inner product let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); - + Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent }) } else { Err("Failed to reach target accuracy".to_string()) @@ -521,9 +521,9 @@ pub fn realize_gram( #[cfg(feature = "dev")] pub mod examples { use std::f64::consts::PI; - + use super::*; - + // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article // below includes a nice translation of the problem statement, which was // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and @@ -547,40 +547,40 @@ 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 for k in 0..4 { problem.frozen.push(3, k, problem.guess[(3, k)]); } - + 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) -> Realization { @@ -601,7 +601,7 @@ pub mod examples { } ).collect::>().as_slice() ); - + const N_POINTS: usize = 2 * N_HINGES; for block in (0..N_POINTS).step_by(2) { let block_next = (block + 2) % N_POINTS; @@ -610,18 +610,18 @@ pub mod examples { 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); } } } - + for k in 0..N_POINTS { problem.frozen.push(3, k, problem.guess[(3, k)]) } - + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } } @@ -630,9 +630,9 @@ pub mod examples { mod tests { use nalgebra::Vector3; use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter}; - + use super::{*, examples::*}; - + #[test] fn freeze_test() { let frozen = PartialMatrix(vec![ @@ -651,7 +651,7 @@ mod tests { ]); assert_eq!(frozen.freeze(&config), expected_result); } - + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -670,7 +670,7 @@ mod tests { ]); assert_eq!(target.sub_proj(&attempt), expected_result); } - + #[test] fn zero_loss_test() { let mut gram = PartialMatrix::new(); @@ -690,7 +690,7 @@ mod tests { let state = SearchState::from_config(&gram, config); assert!(state.loss.abs() < f64::EPSILON); } - + /* TO DO */ // at the frozen indices, the optimization steps should have exact zeros, // and the realized configuration should have the desired values @@ -720,13 +720,13 @@ mod tests { assert_eq!(config[index], value); } } - + #[test] fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; - + // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; @@ -734,7 +734,7 @@ mod tests { assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); } } - + #[test] fn tangent_test_three_spheres() { const SCALED_TOL: f64 = 1.0e-12; @@ -758,7 +758,7 @@ mod tests { let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(config, problem.guess); 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; @@ -786,11 +786,11 @@ mod tests { 0.0, 0.0, -1.0, 0.25, 1.0, ]), ]; - + // confirm that the dimension of the tangent space is no greater than // expected assert_eq!(tangent.basis_std.len(), tangent_motions_std.len()); - + // confirm that the tangent space contains all the motions we expect it // to. since we've already bounded the dimension of the tangent space, // this confirms that the tangent space is what we expect it to be @@ -802,13 +802,13 @@ mod tests { assert!((motion_std - motion_proj).norm_squared() < tol_sq); } } - + fn translation_motion_unif(vel: &Vector3, assembly_dim: usize) -> Vec> { let mut elt_motion = DVector::zeros(4); elt_motion.fixed_rows_mut::<3>(0).copy_from(vel); iter::repeat(elt_motion).take(assembly_dim).collect() } - + fn rotation_motion_unif(ang_vel: &Vector3, points: Vec>) -> Vec> { points.into_iter().map( |pt| { @@ -819,7 +819,7 @@ mod tests { } ).collect() } - + #[test] fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space @@ -827,7 +827,7 @@ mod tests { let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); - + // list some motions that should form a basis for the tangent space of // the solution variety const N_HINGES: usize = 6; @@ -838,12 +838,12 @@ mod tests { translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim), translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim), translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim), - + // the rotations about the coordinate axes rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()), rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()), rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()), - + // the twist motion. more precisely: a motion that keeps the center // of mass stationary and preserves the distances between the // vertices to first order. this has to be the twist as long as: @@ -872,11 +872,11 @@ mod tests { ).collect::>() ) ).collect::>(); - + // confirm that the dimension of the tangent space is no greater than // expected assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len()); - + // confirm that the tangent space contains all the motions we expect it // to. since we've already bounded the dimension of the tangent space, // this confirms that the tangent space is what we expect it to be @@ -888,7 +888,7 @@ mod tests { assert!((motion_std - motion_proj).norm_squared() < tol_sq); } } - + fn translation(dis: Vector3) -> DMatrix { const ELEMENT_DIM: usize = 5; DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ @@ -899,7 +899,7 @@ mod tests { 0.0, 0.0, 0.0, 0.0, 1.0, ]) } - + // confirm that projection onto a configuration subspace is equivariant with // respect to Euclidean motions #[test] @@ -919,7 +919,7 @@ mod tests { let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); assert_eq!(history_orig.scaled_loss.len(), 1); - + // find another pair of spheres that meet at 120°. we'll think of this // solution as a transformed version of the original one let guess_tfm = { @@ -940,17 +940,17 @@ mod tests { let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); assert_eq!(history_tfm.scaled_loss.len(), 1); - + // project a nudge to the tangent space of the solution variety at the // original solution let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0); - + // project the equivalent nudge to the tangent space of the solution // variety at the transformed solution let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]); let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0); - + // take the transformation that sends the original solution to the // transformed solution and apply it to the motion that the original // solution makes in response to the nudge @@ -964,7 +964,7 @@ mod tests { ]); let transl = translation(Vector3::new(0.0, 0.0, 7.0)); let motion_proj_tfm = transl * rot * motion_orig_proj; - + // confirm that the projection of the nudge is equivariant. we loosen // the comparison tolerance because the transformation seems to // introduce some numerical error @@ -972,4 +972,4 @@ mod tests { 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 +} diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index a03b026..d8fb030 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -30,7 +30,7 @@ impl AppState { selection: create_signal(BTreeSet::default()), } } - + // in single-selection mode, select the given element. in multiple-selection // mode, toggle whether the given element is selected fn select(&self, element: &Rc, multi: bool) { @@ -53,10 +53,10 @@ fn main() { // set the console error panic hook #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); - + sycamore::render(|| { provide_context(AppState::new()); - + view! { div(id = "sidebar") { AddRemove {} @@ -66,4 +66,4 @@ fn main() { Display {} } }); -} \ No newline at end of file +} diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index b0f04b5..d54e75c 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -20,7 +20,7 @@ impl SpecifiedValue { pub fn from_empty_spec() -> Self { Self { spec: String::new(), value: None } } - + pub fn is_present(&self) -> bool { matches!(self.value, Some(_)) } @@ -42,7 +42,7 @@ impl From> for SpecifiedValue { // if the specification is properly formatted, and `Error` if not impl TryFrom for SpecifiedValue { type Error = ParseFloatError; - + fn try_from(spec: String) -> Result { if spec.is_empty() { Ok(Self::from_empty_spec()) @@ -52,4 +52,4 @@ impl TryFrom for SpecifiedValue { ) } } -} \ No newline at end of file +} -- 2.43.0 From b89fa02f528a26c58e32ba332074b6f73197ee26 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 14 Oct 2025 12:42:51 -0700 Subject: [PATCH 10/11] chore: remove trailing whitespace outside of app-proto/src as well --- .forgejo/setup-trunk/action.yaml | 2 +- .forgejo/workflows/continuous-integration.yaml | 4 ++-- README.md | 6 +++--- app-proto/examples/irisawa-hexlet.rs | 4 ++-- app-proto/examples/kaleidocycle.rs | 4 ++-- app-proto/index.html | 2 +- notes/inversive.md | 4 ++-- notes/theory.md | 9 ++++----- src/index.html | 2 -- 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.forgejo/setup-trunk/action.yaml b/.forgejo/setup-trunk/action.yaml index 6007527..85e8a31 100644 --- a/.forgejo/setup-trunk/action.yaml +++ b/.forgejo/setup-trunk/action.yaml @@ -10,7 +10,7 @@ runs: using: "composite" steps: - run: rustup target add wasm32-unknown-unknown - + # install the Trunk binary to `ci-bin` within the workspace directory, which # is determined by the `github.workspace` label and reflected in the # `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml index f3b0130..6b53b3a 100644 --- a/.forgejo/workflows/continuous-integration.yaml +++ b/.forgejo/workflows/continuous-integration.yaml @@ -24,6 +24,6 @@ jobs: # workspace directory (action variable `github.workspace`, environment # variable `$GITHUB_WORKSPACE`): - uses: https://code.forgejo.org/actions/checkout@v4 - + - uses: ./.forgejo/setup-trunk - - run: RUSTFLAGS='-D warnings' cargo test \ No newline at end of file + - run: RUSTFLAGS='-D warnings' cargo test diff --git a/README.md b/README.md index ac2771b..16b0d35 100644 --- a/README.md +++ b/README.md @@ -52,20 +52,20 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter 1. Use `sh` to run the script `tools/run-examples.sh`. - The script is location-independent, so you can do this from anywhere in the dyna3 repository. - The call from the top level of the repository is: - + ```bash sh tools/run-examples.sh ``` - For each example problem, the engine will print the value of the loss function at each optimization step. - The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then execute - + ```julia include("irisawa-hexlet.jl") for (step, scaled_loss) in enumerate(history_alt.scaled_loss) println(rpad(step-1, 4), " | ", scaled_loss) end ``` - + you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show. ### Run the automated tests diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 0d710ff..d7dd5fc 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -15,9 +15,9 @@ fn main() { for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } - + // print the completed Gram matrix print::gram_matrix(&config); } print::loss_history(&realization.history); -} \ No newline at end of file +} diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index ae4eb07..4a9ad7e 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -14,7 +14,7 @@ fn main() { // print the completed Gram matrix and the realized configuration print::gram_matrix(&config); print::config(&config); - + // find the kaleidocycle's twist motion by projecting onto the tangent // space const N_POINTS: usize = 12; @@ -29,4 +29,4 @@ fn main() { let normalization = 5.0 / twist_motion[(2, 0)]; println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); } -} \ No newline at end of file +} diff --git a/app-proto/index.html b/app-proto/index.html index 4fbe52f..c312b51 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,7 +6,7 @@ - +