From 627cea455ca3790e03969436e02eeaa5ad1947de Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 10 Oct 2025 22:48:46 -0700 Subject: [PATCH 1/3] chore: Properly punctuate README --- README.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index ec3d440..ac2771b 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ Note that currently this is just the barest beginnings of the project, more of a ### Implementation goals -* Comfortable, intuitive UI +* Provide a comfortable, intuitive UI -* Able to run in browser (so implemented in WASM-compatible language) +* Allow execution in browser (so implemented in WASM-compatible language) -* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well. +* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well ## Prototype @@ -24,40 +24,40 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter ### Install the prerequisites -1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager - - It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup) -2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain" - - If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you -3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html) -4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/) -5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool - - In the future, `trunk` can be updated with the same command. You may need the `--locked` flag if your ambient version of `rustc` does not match that required by `trunk`. +1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager. + - It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup). +2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain". + - If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you. +3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html). +4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/). +5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool. + - In the future, `trunk` can be updated with the same command. (You may need the `--locked` flag if your ambient version of `rustc` does not match that required by `trunk`.) 6. Add the `.cargo/bin` folder in your home directory to your executable search path - - This lets you call Trunk, and other tools installed by Cargo, without specifying their paths - - On POSIX systems, the search path is stored in the `PATH` environment variable - - Alternatively, if you don't want to adjust your `PATH`, you can install `trunk` in another directory `DIR` via `cargo install --root DIR trunk` + - This lets you call Trunk, and other tools installed by Cargo, without specifying their paths. + - On POSIX systems, the search path is stored in the `PATH` environment variable. + - Alternatively, if you don't want to adjust your `PATH`, you can install `trunk` in another directory `DIR` via `cargo install --root DIR trunk`. ### Play with the prototype -1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype - - The crates the prototype depends on will be downloaded and served automatically - - For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag +1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype. + - The crates the prototype depends on will be downloaded and served automatically. + - For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag. - If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead. -3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:` - - Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype -4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype +3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`. + - Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype. +4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype. ### Run the engine on some example problems -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 +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 + - 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") @@ -66,24 +66,24 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter 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 + 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 -1. Go into the `app-proto` folder -2. Call `cargo test` +1. Go into the `app-proto` folder. +2. Call `cargo test`. ### Deploy the prototype -1. From the `app-proto` folder, call `trunk build --release` - - Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build - - If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead +1. From the `app-proto` folder, call `trunk build --release`. + - Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build. + - If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead. 2. Use `sh` to run the packaging script `tools/package-for-deployment.sh`. - - The script is location-independent, so you can do this from anywhere in the dyna3 repository + - 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/package-for-deployment.sh ``` - - This will overwrite or replace the files in `deploy/dyna3` + - This will overwrite or replace the files in `deploy/dyna3`. 3. Put the contents of `deploy/dyna3` in the folder on your server that the prototype will be served from. - - To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path \ No newline at end of file + - To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path. From 6dbbe2ce2d9ed63a1a92705a1c18b32da0cb1921 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 10 Oct 2025 22:57:52 -0700 Subject: [PATCH 2/3] chore: Hopefully final formatting items from review --- app-proto/src/assembly.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0d2a510..0264b75 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -688,7 +688,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); } @@ -956,7 +958,7 @@ mod tests { #[test] #[should_panic(expected = "Subject \"sphere1\" must be indexed before \ -inversive distance regulator writes problem data")] + inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { let subjects = [0, 1].map( From a4101ab81c1106e6e99d1b1dc3461b4c76015c06 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 10 Oct 2025 10:20:38 -0700 Subject: [PATCH 3/3] 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 0264b75..d07c464 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 } @@ -601,7 +601,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 @@ -613,13 +613,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, @@ -639,7 +639,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(); @@ -653,7 +653,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(); @@ -665,12 +665,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 @@ -680,13 +680,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()) @@ -696,7 +696,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(); @@ -708,17 +708,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() @@ -726,7 +726,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:"); @@ -749,9 +749,9 @@ impl Assembly { } }); } - + // --- updating the configuration --- - + pub fn load_config(&self, config: &DMatrix) { for elt in self.elements.get_clone_untracked() { elt.representation().update( @@ -759,9 +759,9 @@ impl Assembly { ); } } - + // --- realization --- - + pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { @@ -769,7 +769,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()); @@ -783,21 +783,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 { @@ -809,20 +809,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 { @@ -832,7 +832,7 @@ impl Assembly { SpecifiedValue::from_empty_spec() } ); - + // save the tangent space self.tangent.set_silent(tangent); }, @@ -842,15 +842,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: @@ -867,7 +867,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. @@ -885,7 +885,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 @@ -896,7 +896,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 @@ -914,7 +914,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() { @@ -932,7 +932,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 @@ -943,9 +943,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")] @@ -955,7 +955,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")] @@ -973,7 +973,7 @@ mod tests { }.pose(&mut ConstraintProblem::new(2)); }); } - + #[test] fn curvature_drift_test() { const INITIAL_RADIUS: f64 = 0.25; @@ -993,7 +993,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; @@ -1009,7 +1009,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 +}