From a4565281d5c3e7042ab9e1a2bdaa22bc52126b08 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 7 Aug 2025 23:24:07 +0000 Subject: [PATCH 1/4] Refactor: rename loaders and adopt 'Self' type convention (#111) Resolves #109. Resolves #110. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/111 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 26 ++++++------ app-proto/src/components/diagnostics.rs | 4 +- app-proto/src/components/display.rs | 12 +++--- .../src/components/test_assembly_chooser.rs | 42 +++++++++---------- app-proto/src/engine.rs | 32 +++++++------- app-proto/src/main.rs | 4 +- app-proto/src/specified.rs | 8 ++-- 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 43066fd..94e7b3c 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -175,8 +175,8 @@ impl Sphere { label: String, color: ElementColor, representation: DVector, - ) -> Sphere { - Sphere { + ) -> Self { + Self { id, label, color, @@ -194,8 +194,8 @@ impl Element for Sphere { "sphere".to_string() } - fn default(id: String, id_num: u64) -> Sphere { - Sphere::new( + fn default(id: String, id_num: u64) -> Self { + Self::new( id, format!("Sphere {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], @@ -275,8 +275,8 @@ impl Point { label: String, color: ElementColor, representation: DVector, - ) -> Point { - Point { + ) -> Self { + Self { id, label, color, @@ -294,8 +294,8 @@ impl Element for Point { "point".to_string() } - fn default(id: String, id_num: u64) -> Point { - Point::new( + fn default(id: String, id_num: u64) -> Self { + Self::new( id, format!("Point {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], @@ -348,7 +348,7 @@ impl ProblemPoser for Point { format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() ); problem.gram.push_sym(index, index, 0.0); - problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5); + problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } @@ -393,7 +393,7 @@ pub struct InversiveDistanceRegulator { } impl InversiveDistanceRegulator { - pub fn new(subjects: [Rc; 2]) -> InversiveDistanceRegulator { + pub fn new(subjects: [Rc; 2]) -> Self { let representations = subjects.each_ref().map(|subj| subj.representation()); let measurement = create_memo(move || { representations[0].with(|rep_0| @@ -406,7 +406,7 @@ impl InversiveDistanceRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - InversiveDistanceRegulator { subjects, measurement, set_point, serial } + Self { subjects, measurement, set_point, serial } } } @@ -453,7 +453,7 @@ pub struct HalfCurvatureRegulator { } impl HalfCurvatureRegulator { - pub fn new(subject: Rc) -> HalfCurvatureRegulator { + pub fn new(subject: Rc) -> Self { let measurement = subject.representation().map( |rep| rep[Sphere::CURVATURE_COMPONENT] ); @@ -461,7 +461,7 @@ impl HalfCurvatureRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - HalfCurvatureRegulator { subject, measurement, set_point, serial } + Self { subject, measurement, set_point, serial } } } diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index b274dca..e265982 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -15,8 +15,8 @@ struct DiagnosticsState { } impl DiagnosticsState { - fn new(initial_tab: String) -> DiagnosticsState { - DiagnosticsState { active_tab: create_signal(initial_tab) } + fn new(initial_tab: String) -> Self { + Self { active_tab: create_signal(initial_tab) } } } diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index a0cdba6..da921dd 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -41,8 +41,8 @@ struct SceneSpheres { } impl SceneSpheres { - fn new() -> SceneSpheres { - SceneSpheres { + fn new() -> Self { + Self { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), @@ -71,8 +71,8 @@ struct ScenePoints { } impl ScenePoints { - fn new() -> ScenePoints { - ScenePoints { + fn new() -> Self { + Self { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), @@ -97,8 +97,8 @@ pub struct Scene { } impl Scene { - fn new() -> Scene { - Scene { + fn new() -> Self { + Self { spheres: SceneSpheres::new(), points: ScenePoints::new(), } diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index 5ed94ad..0d387d3 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -26,7 +26,7 @@ use crate::{ // done more work on saving and loading assemblies, we should come back to this // code to see if it can be simplified -fn load_gen_assemb(assembly: &Assembly) { +fn load_general(assembly: &Assembly) { let _ = assembly.try_insert_element( Sphere::new( String::from("gemini_a"), @@ -77,7 +77,7 @@ fn load_gen_assemb(assembly: &Assembly) { ); } -fn load_low_curv_assemb(assembly: &Assembly) { +fn load_low_curvature(assembly: &Assembly) { // create the spheres let a = 0.75_f64.sqrt(); let _ = assembly.try_insert_element( @@ -196,7 +196,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { } } -fn load_pointed_assemb(assembly: &Assembly) { +fn load_pointed(assembly: &Assembly) { let _ = assembly.try_insert_element( Point::new( format!("point_front"), @@ -246,7 +246,7 @@ fn load_pointed_assemb(assembly: &Assembly) { // B-C " // C-C " // A-C -0.25 * φ^2 = -0.6545084971874737 -fn load_tridim_icosahedron_assemb(assembly: &Assembly) { +fn load_tridiminished_icosahedron(assembly: &Assembly) { // create the vertices const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32]; const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; @@ -409,7 +409,7 @@ fn load_tridim_icosahedron_assemb(assembly: &Assembly) { // to finish describing the dodecahedral circle packing, set the inversive // distance regulators to -1. some of the regulators have already been set -fn load_dodeca_packing_assemb(assembly: &Assembly) { +fn load_dodecahedral_packing(assembly: &Assembly) { // add the substrate let _ = assembly.try_insert_element( Sphere::new( @@ -550,7 +550,7 @@ fn load_dodeca_packing_assemb(assembly: &Assembly) { // the initial configuration of this test assembly deliberately violates the // constraints, so loading the assembly will trigger a non-trivial realization -fn load_balanced_assemb(assembly: &Assembly) { +fn load_balanced(assembly: &Assembly) { // create the spheres const R_OUTER: f64 = 10.0; const R_INNER: f64 = 4.0; @@ -611,7 +611,7 @@ fn load_balanced_assemb(assembly: &Assembly) { // the initial configuration of this test assembly deliberately violates the // constraints, so loading the assembly will trigger a non-trivial realization -fn load_off_center_assemb(assembly: &Assembly) { +fn load_off_center(assembly: &Assembly) { // create a point almost at the origin and a sphere centered on the origin let _ = assembly.try_insert_element( Point::new( @@ -648,7 +648,7 @@ fn load_off_center_assemb(assembly: &Assembly) { // sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an // inversive distance of -1 between the insphere and each face, and then set an // inversive distance of 0 between the circumsphere and each vertex -fn load_radius_ratio_assemb(assembly: &Assembly) { +fn load_radius_ratio(assembly: &Assembly) { let index_range = 1..=4; // create the spheres @@ -789,7 +789,7 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { // conditions are exactly representable as floats, unlike the analogous numbers // in the scaled-up problem. the inexact representations might break the // symmetry that's getting the engine stuck -fn load_irisawa_hexlet_assemb(assembly: &Assembly) { +fn load_irisawa_hexlet(assembly: &Assembly) { let index_range = 1..=6; let colors = [ [1.00_f32, 0.00_f32, 0.25_f32], @@ -909,15 +909,15 @@ pub fn TestAssemblyChooser() -> View { // load assembly match name.as_str() { - "general" => load_gen_assemb(assembly), - "low-curv" => load_low_curv_assemb(assembly), - "pointed" => load_pointed_assemb(assembly), - "tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly), - "dodeca-packing" => load_dodeca_packing_assemb(assembly), - "balanced" => load_balanced_assemb(assembly), - "off-center" => load_off_center_assemb(assembly), - "radius-ratio" => load_radius_ratio_assemb(assembly), - "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), + "general" => load_general(assembly), + "low-curvature" => load_low_curvature(assembly), + "pointed" => load_pointed(assembly), + "tridiminished-icosahedron" => load_tridiminished_icosahedron(assembly), + "dodecahedral-packing" => load_dodecahedral_packing(assembly), + "balanced" => load_balanced(assembly), + "off-center" => load_off_center(assembly), + "radius-ratio" => load_radius_ratio(assembly), + "irisawa-hexlet" => load_irisawa_hexlet(assembly), _ => (), }; }); @@ -927,10 +927,10 @@ pub fn TestAssemblyChooser() -> View { view! { select(bind:value = assembly_name) { option(value = "general") { "General" } - option(value = "low-curv") { "Low-curvature" } + option(value = "low-curvature") { "Low-curvature" } option(value = "pointed") { "Pointed" } - option(value = "tridim-icosahedron") { "Tridiminished icosahedron" } - option(value = "dodeca-packing") { "Dodecahedral packing" } + option(value = "tridiminished-icosahedron") { "Tridiminished icosahedron" } + option(value = "dodecahedral-packing") { "Dodecahedral packing" } option(value = "balanced") { "Balanced" } option(value = "off-center") { "Off-center" } option(value = "radius-ratio") { "Radius ratio" } diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index dc6b470..d033c01 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -59,12 +59,12 @@ pub struct MatrixEntry { pub struct PartialMatrix(Vec); impl PartialMatrix { - pub fn new() -> PartialMatrix { - PartialMatrix(Vec::::new()) + pub fn new() -> Self { + Self(Vec::::new()) } pub fn push(&mut self, row: usize, col: usize, value: f64) { - let PartialMatrix(entries) = self; + let Self(entries) = self; entries.push(MatrixEntry { index: (row, col), value }); } @@ -114,7 +114,7 @@ impl IntoIterator for PartialMatrix { type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - let PartialMatrix(entries) = self; + let Self(entries) = self; entries.into_iter() } } @@ -139,8 +139,8 @@ pub struct ConfigSubspace { } impl ConfigSubspace { - pub fn zero(assembly_dim: usize) -> ConfigSubspace { - ConfigSubspace { + pub fn zero(assembly_dim: usize) -> Self { + Self { assembly_dim, basis_proj: Vec::new(), basis_std: Vec::new(), @@ -154,7 +154,7 @@ impl ConfigSubspace { a: DMatrix, proj_to_std: DMatrix, assembly_dim: usize, - ) -> ConfigSubspace { + ) -> Self { // find a basis for the kernel. the basis is expressed in the projection // coordinates, and it's orthonormal with respect to the projection // inner product @@ -173,7 +173,7 @@ impl ConfigSubspace { const ELEMENT_DIM: usize = 5; const UNIFORM_DIM: usize = 4; - ConfigSubspace { + Self { assembly_dim, basis_std: basis_std.column_iter().map( |v| Into::>::into( @@ -224,8 +224,8 @@ pub struct DescentHistory { } impl DescentHistory { - pub fn new() -> DescentHistory { - DescentHistory { + pub fn new() -> Self { + Self { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), @@ -245,9 +245,9 @@ pub struct ConstraintProblem { } impl ConstraintProblem { - pub fn new(element_count: usize) -> ConstraintProblem { + pub fn new(element_count: usize) -> Self { const ELEMENT_DIM: usize = 5; - ConstraintProblem { + Self { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), guess: DMatrix::::zeros(ELEMENT_DIM, element_count), @@ -255,8 +255,8 @@ impl ConstraintProblem { } #[cfg(feature = "dev")] - pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { - ConstraintProblem { + pub fn from_guess(guess_columns: &[DVector]) -> Self { + Self { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), guess: DMatrix::from_columns(guess_columns), @@ -284,10 +284,10 @@ struct SearchState { } impl SearchState { - fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> Self { let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let loss = err_proj.norm_squared(); - SearchState { config, err_proj, loss } + Self { config, err_proj, loss } } } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 7ca0731..a03b026 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -24,8 +24,8 @@ struct AppState { } impl AppState { - fn new() -> AppState { - AppState { + fn new() -> Self { + Self { assembly: Assembly::new(), selection: create_signal(BTreeSet::default()), } diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index ea1731c..788460b 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -17,8 +17,8 @@ pub struct SpecifiedValue { } impl SpecifiedValue { - pub fn from_empty_spec() -> SpecifiedValue { - SpecifiedValue { spec: String::new(), value: None } + pub fn from_empty_spec() -> Self { + Self { spec: String::new(), value: None } } pub fn is_present(&self) -> bool { @@ -34,10 +34,10 @@ impl TryFrom for SpecifiedValue { fn try_from(spec: String) -> Result { if spec.is_empty() { - Ok(SpecifiedValue::from_empty_spec()) + Ok(Self::from_empty_spec()) } else { spec.parse::().map( - |value| SpecifiedValue { spec, value: Some(value) } + |value| Self { spec, value: Some(value) } ) } } From af18a8e7d19067523de7bb4c8555c397a6f13d87 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 11 Aug 2025 03:33:19 +0000 Subject: [PATCH 2/4] Write a deployment packaging script (#113) Adds a packaging script to help automate deployment. Documents the deployment process in `README.md`. Also, moves `run-examples.sh` into the tools folder that we created for the packaging script. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/113 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- README.md | 46 ++++++++++++++++++++-------- app-proto/Trunk.toml | 2 ++ deploy/.gitignore | 5 +++ tools/package-for-deployment.sh | 16 ++++++++++ {app-proto => tools}/run-examples.sh | 2 +- 5 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 app-proto/Trunk.toml create mode 100644 deploy/.gitignore create mode 100644 tools/package-for-deployment.sh rename {app-proto => tools}/run-examples.sh (89%) diff --git a/README.md b/README.md index 3a29eb0..cf3e589 100644 --- a/README.md +++ b/README.md @@ -25,32 +25,37 @@ 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) + - 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 + - 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 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 + - 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 ### 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* - * *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead. + - 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* + - 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. Go into the `app-proto` folder -2. Call `./run-examples` - * *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* +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 ```julia include("irisawa-hexlet.jl") @@ -59,9 +64,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` + +### 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 +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 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` +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 diff --git a/app-proto/Trunk.toml b/app-proto/Trunk.toml new file mode 100644 index 0000000..017deba --- /dev/null +++ b/app-proto/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "./" \ No newline at end of file diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..192f529 --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1,5 @@ +/dyna3.zip +/dyna3/index.html +/dyna3/dyna3-*.js +/dyna3/dyna3-*.wasm +/dyna3/main-*.css \ No newline at end of file diff --git a/tools/package-for-deployment.sh b/tools/package-for-deployment.sh new file mode 100644 index 0000000..fdda434 --- /dev/null +++ b/tools/package-for-deployment.sh @@ -0,0 +1,16 @@ +# set paths. this technique for getting the script location comes from +# `mklement0` on Stack Overflow +# +# https://stackoverflow.com/a/24114056 +# +TOOLS=$(dirname -- $0) +SRC="$TOOLS/../app-proto/dist" +DEST="$TOOLS/../deploy/dyna3" + +# remove the old hash-named files +[ -e "$DEST"/dyna3-*.js ] && rm "$DEST"/dyna3-*.js +[ -e "$DEST"/dyna3-*.wasm ] && rm "$DEST"/dyna3-*.wasm +[ -e "$DEST"/main-*.css ] && rm "$DEST"/main-*.css + +# copy the distribution +cp -r "$SRC/." "$DEST" diff --git a/app-proto/run-examples.sh b/tools/run-examples.sh similarity index 89% rename from app-proto/run-examples.sh rename to tools/run-examples.sh index 861addf..0946d92 100644 --- a/app-proto/run-examples.sh +++ b/tools/run-examples.sh @@ -8,7 +8,7 @@ # the application prototype # find the manifest file for the application prototype -MANIFEST="$(dirname -- $0)/Cargo.toml" +MANIFEST="$(dirname -- $0)/../app-proto/Cargo.toml" # set up the command that runs each example RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" From 978f70aac764f0457fbe4f89d8157ff5e396df81 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 18 Sep 2025 23:31:17 +0000 Subject: [PATCH 3/4] Rewind through the descent history (#114) You can now rewind through the descent history of the last realization using the *Step* control that's been added to the diagnostics panel. The starting value of the *Step* control depends on the realization status. After a successful realization, we show the realized state (the last step). After an unsuccessful realization, we show the initial guess (step zero). Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/114 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/main.css | 9 ++++ app-proto/src/assembly.rs | 59 ++++++++++++++++------ app-proto/src/components/diagnostics.rs | 66 ++++++++++++++++++++++++- app-proto/src/components/display.rs | 20 +++++++- app-proto/src/engine.rs | 6 +-- app-proto/src/specified.rs | 11 +++++ 6 files changed, 152 insertions(+), 19 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 7981285..a00d309 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -184,6 +184,7 @@ details[open]:has(li) .element-switch::after { #diagnostics-bar { display: flex; + gap: 8px; } #realization-status { @@ -207,6 +208,14 @@ details[open]:has(li) .element-switch::after { content: '⚠'; } +#step-input > label { + padding-right: 4px; +} + +#step-input > input { + width: 45px; +} + .diagnostics-panel { margin-top: 10px; min-height: 180px; diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 94e7b3c..669c0d0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -534,6 +534,7 @@ pub struct Assembly { // realization diagnostics pub realization_status: Signal>, pub descent_history: Signal, + pub step: Signal, } impl Assembly { @@ -547,20 +548,33 @@ impl Assembly { realization_trigger: create_signal(()), realization_status: create_signal(Ok(())), 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_effect = assembly.clone(); + let assembly_for_realization = assembly.clone(); create_effect(move || { - assembly_for_effect.elements.track(); - assembly_for_effect.regulators.with( + assembly_for_realization.elements.track(); + assembly_for_realization.regulators.with( |regs| for reg in regs { reg.set_point().track(); } ); - assembly_for_effect.realization_trigger.track(); - assembly_for_effect.realize(); + 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(); + create_effect(move || { + if let Some(step) = assembly.step.with(|n| n.value) { + let config = assembly.descent_history.with_untracked( + |history| history.config[step as usize].clone() + ); + assembly_for_step_selection.load_config(&config) + } }); assembly @@ -647,6 +661,16 @@ impl Assembly { }); } + // --- updating the configuration --- + + 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())) + ); + } + } + // --- realization --- pub fn realize(&self) { @@ -696,11 +720,12 @@ impl Assembly { console_log!("Loss: {}", history.scaled_loss.last().unwrap()); } - // report the loss history + // report the descent history + let step_cnt = history.config.len(); self.descent_history.set(history); match result { - Ok(ConfigNeighborhood { config, nbhd: tangent }) => { + Ok(ConfigNeighborhood { nbhd: tangent, .. }) => { /* DEBUG */ // report the tangent dimension console_log!("Tangent dimension: {}", tangent.dim()); @@ -708,12 +733,15 @@ impl Assembly { // report the realization status self.realization_status.set(Ok(())); - // read out the solution - for elt in self.elements.get_clone_untracked() { - elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) - ); - } + // display the last realization step + self.step.set( + if step_cnt > 0 { + let last_step = step_cnt - 1; + SpecifiedValue::try_from(last_step.to_string()).unwrap() + } else { + SpecifiedValue::from_empty_spec() + } + ); // save the tangent space self.tangent.set_silent(tangent); @@ -723,7 +751,10 @@ impl Assembly { // setting the status to has a different type than the // `Err(message)` we received from the match: we're changing the // `Ok` type from `Realization` to `()` - self.realization_status.set(Err(message)) + self.realization_status.set(Err(message)); + + // display the initial guess + self.step.set(SpecifiedValue::from(Some(0.0))); }, } } diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index e265982..51d58f1 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -7,7 +7,7 @@ use charming::{ }; use sycamore::prelude::*; -use crate::AppState; +use crate::{AppState, specified::SpecifiedValue}; #[derive(Clone)] struct DiagnosticsState { @@ -48,6 +48,69 @@ fn RealizationStatus() -> View { } } +// history step input +#[component] +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() { + 0 => None, + n => Some(n - 1), + } + ); + 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" } + input( + r#type = "number", + min = "0", + max = input_max.with(|max| max.to_string()), + bind:value = value, + bind:valueAsNumber = value_as_number, + on:change = move |_| { + if last_step.with(|last| last.is_some()) { + // clamp the step within its allowed range. the lower + // bound is redundant on browsers that make it + // impossible to type negative values into a number + // input with a non-negative `min`, but there's no harm + // in being careful + let step_raw = value.with( + |val| SpecifiedValue::try_from(val.clone()) + .unwrap_or(SpecifiedValue::from_empty_spec() + ) + ); + let step = SpecifiedValue::from( + step_raw.value.map( + |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); + } else { + value.set(String::new()); + } + }, + ) + } + } +} + fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { vec![ Some(step as f64), @@ -248,6 +311,7 @@ pub fn Diagnostics() -> View { option(value = "loss") { "Loss" } option(value = "spectrum") { "Spectrum" } } + StepInput {} } DiagnosticsPanel(name = "loss") { LossHistory {} } DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index da921dd..98be85e 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -588,7 +588,25 @@ pub fn Display() -> View { location_z *= (time_step * ZOOM_SPEED * zoom).exp(); // manipulate the assembly - if state.selection.with(|sel| sel.len() == 1) { + /* KLUDGE */ + // to avoid the complexity of making tangent space projection + // conditional and dealing with unnormalized representation vectors, + // we only allow manipulation when we're looking at the last step of + // a successful realization + let realization_successful = state.assembly.realization_status.with( + |status| status.is_ok() + ); + let step_val = state.assembly.step.with_untracked(|step| step.value); + let on_init_step = step_val.is_some_and(|n| n == 0.0); + let on_last_step = step_val.is_some_and( + |n| state.assembly.descent_history.with_untracked( + |history| n as usize + 1 == history.config.len().max(1) + ) + ); + let on_manipulable_step = + !realization_successful && on_init_step + || realization_successful && on_last_step; + if on_manipulable_step && state.selection.with(|sel| sel.len() == 1) { let sel = state.selection.with( |sel| sel.into_iter().next().unwrap().clone() ); diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index d033c01..ef150a0 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -353,7 +353,7 @@ fn seek_better_config( // a first-order neighborhood of a configuration pub struct ConfigNeighborhood { - pub config: DMatrix, + #[cfg(feature = "dev")] pub config: DMatrix, pub nbhd: ConfigSubspace, } @@ -388,7 +388,7 @@ pub fn realize_gram( if assembly_dim == 0 { let result = Ok( ConfigNeighborhood { - config: guess.clone(), + #[cfg(feature = "dev")] config: guess.clone(), nbhd: ConfigSubspace::zero(0), } ); @@ -509,7 +509,7 @@ pub fn realize_gram( // find the kernel of the Hessian. give it the uniform inner product let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); - Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) + Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent }) } else { Err("Failed to reach target accuracy".to_string()) }; diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index 788460b..b0f04b5 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -26,6 +26,17 @@ impl SpecifiedValue { } } +// a `SpecifiedValue` can be constructed from a floating-point option, which is +// given a canonical specification +impl From> for SpecifiedValue { + fn from(value: Option) -> Self { + match value { + Some(x) => SpecifiedValue{ spec: x.to_string(), value }, + None => SpecifiedValue::from_empty_spec(), + } + } +} + // a `SpecifiedValue` can be constructed from a specification string, formatted // as described in the comment on the structure definition. the result is `Ok` // if the specification is properly formatted, and `Error` if not From 2c8c09d20d3567a968acf8e839e6d04c1414751b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 13 Oct 2025 22:52:02 +0000 Subject: [PATCH 4/4] feat: Point coordinate regulators (#118) Implement 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`. Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- README.md | 64 +++++++------- app-proto/Cargo.lock | 21 +++++ app-proto/Cargo.toml | 1 + app-proto/src/assembly.rs | 124 ++++++++++++++++++++++++---- app-proto/src/components/outline.rs | 15 ++++ app-proto/src/engine.rs | 4 +- 6 files changed, 180 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index cf3e589..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,38 +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 +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 + - 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") @@ -64,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. diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 4f75c45..731dd84 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -255,6 +255,7 @@ dependencies = [ "charming", "console_error_panic_hook", "dyna3", + "enum-iterator", "itertools", "js-sys", "lazy_static", @@ -271,6 +272,26 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.1" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 1230b47..d5221a1 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -10,6 +10,7 @@ default = ["console_error_panic_hook"] dev = [] [dependencies] +enum-iterator = "2.3.0" itertools = "0.13.0" js-sys = "0.3.70" lazy_static = "1.5.0" diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 669c0d0..0264b75 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,10 +1,11 @@ +use enum_iterator::{all, Sequence}; use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ cell::Cell, cmp::Ordering, collections::{BTreeMap, BTreeSet}, fmt, - fmt::{Debug, Formatter}, + fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, rc::Rc, sync::{atomic, atomic::AtomicU64}, @@ -26,6 +27,7 @@ use crate::{ ConfigSubspace, ConstraintProblem, DescentHistory, + MatrixEntry, Realization, }, specified::SpecifiedValue, @@ -84,6 +86,14 @@ impl Ord for dyn Serial { } } +// Small helper function to generate consistent errors when there +// are indexing issues in a ProblemPoser +fn indexing_error(item: &str, name: &str, actor: &str) -> String { + format!( + "{item} \"{name}\" must be indexed before {actor} writes problem data" + ) +} + pub trait ProblemPoser { fn pose(&self, problem: &mut ConstraintProblem); } @@ -125,8 +135,8 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { } impl Debug for dyn Element { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { - self.id().fmt(f) + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.id(), f) } } @@ -249,8 +259,7 @@ impl Serial for Sphere { impl ProblemPoser for Sphere { fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( - format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str() - ); + 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()); } @@ -269,6 +278,7 @@ pub struct Point { impl Point { const WEIGHT_COMPONENT: usize = 3; + const NORM_COMPONENT: usize = 4; pub fn new( id: String, @@ -302,6 +312,15 @@ 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:: + }) + .collect() + } fn id(&self) -> &String { &self.id @@ -345,8 +364,7 @@ impl Serial for Point { impl ProblemPoser for Point { fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( - format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() - ); + 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()); @@ -436,8 +454,8 @@ impl ProblemPoser for InversiveDistanceRegulator { if let Some(val) = set_pt.value { let [row, col] = self.subjects.each_ref().map( |subj| subj.column_index().expect( - "Subjects should be indexed before inversive distance regulator writes problem data" - ) + indexing_error("Subject", subj.id(), + "inversive distance regulator").as_str()) ); problem.gram.push_sym(row, col, val); } @@ -446,14 +464,14 @@ impl ProblemPoser for InversiveDistanceRegulator { } pub struct HalfCurvatureRegulator { - pub subject: Rc, + pub subject: Rc, pub measurement: ReadSignal, pub set_point: Signal, serial: u64, } impl HalfCurvatureRegulator { - pub fn new(subject: Rc) -> Self { + pub fn new(subject: Rc) -> Self { let measurement = subject.representation().map( |rep| rep[Sphere::CURVATURE_COMPONENT] ); @@ -490,14 +508,85 @@ impl ProblemPoser for HalfCurvatureRegulator { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { let col = self.subject.column_index().expect( - "Subject should be indexed before half-curvature regulator writes problem data" - ); + indexing_error("Subject", &self.subject.id, + "half-curvature regulator").as_str()); problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); } }); } } +#[derive(Clone, Copy, Sequence)] +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" } + } +} + +impl Display for Axis { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +pub struct PointCoordinateRegulator { + pub subject: Rc, + pub axis: Axis, + pub measurement: ReadSignal, + pub set_point: Signal, + serial: u64 +} + +impl PointCoordinateRegulator { + pub fn new(subject: Rc, axis: Axis) -> Self { + 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() } + } +} + +impl Serial for PointCoordinateRegulator { + 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 } +} + +impl ProblemPoser for PointCoordinateRegulator { + fn pose(&self, problem: &mut ConstraintProblem) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let col = self.subject.column_index().expect( + indexing_error("Subject", &self.subject.id, + "point-coordinate regulator").as_str()); + problem.frozen.push(self.axis as usize, col, val); + // If all three of the subject's spatial coordinates have been + // frozen, then freeze its norm component: + let mut coords = [0.0; Axis::CARDINALITY]; + let mut nset: usize = 0; + for &MatrixEntry {index, value} in &(problem.frozen) { + if index.1 == col && index.0 < Axis::CARDINALITY { + nset += 1; + 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]); + } + } + }); + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub element: Rc, @@ -698,6 +787,7 @@ impl Assembly { /* 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 @@ -857,7 +947,8 @@ mod tests { use crate::engine; #[test] - #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] + #[should_panic(expected = + "Sphere \"sphere\" must be indexed before it writes problem data")] fn unindexed_element_test() { let _ = create_root(|| { let elt = Sphere::default("sphere".to_string(), 0); @@ -866,7 +957,8 @@ mod tests { } #[test] - #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] + #[should_panic(expected = "Subject \"sphere1\" must be indexed before \ + inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { let subjects = [0, 1].map( @@ -927,4 +1019,4 @@ mod tests { assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); }); } -} \ No newline at end of file +} diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs index 5355042..547b73b 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/components/outline.rs @@ -9,6 +9,7 @@ use crate::{ Element, HalfCurvatureRegulator, InversiveDistanceRegulator, + PointCoordinateRegulator, Regulator, }, specified::SpecifiedValue @@ -119,6 +120,20 @@ impl OutlineItem for HalfCurvatureRegulator { } } +impl OutlineItem for PointCoordinateRegulator { + fn outline_item(self: Rc, _element: &Rc) -> View { + let name = format!("{} coordinate", self.axis); + view! { + li(class = "regulator") { + div(class = "regulator-label") // for spacing + div(class = "regulator-type") { (name) } + RegulatorInput(regulator = self) + div(class = "status") + } + } + } +} + // a list item that shows an element in an outline view of an assembly #[component(inline_props)] fn ElementOutlineItem(element: Rc) -> View { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index ef150a0..0f26f02 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -52,8 +52,8 @@ pub fn project_point_to_normalized(rep: &mut DVector) { // --- partial matrices --- pub struct MatrixEntry { - index: (usize, usize), - value: f64, + pub index: (usize, usize), + pub value: f64, } pub struct PartialMatrix(Vec);