diff --git a/README.md b/README.md index cf3e589..3a29eb0 100644 --- a/README.md +++ b/README.md @@ -25,37 +25,32 @@ 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. 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 +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* ```julia include("irisawa-hexlet.jl") @@ -64,24 +59,9 @@ 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/Cargo.lock b/app-proto/Cargo.lock index 4f75c45..3bf609c 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -20,21 +20,6 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "approx" version = "0.5.1" @@ -50,21 +35,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -98,35 +68,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "charming" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09" -dependencies = [ - "handlebars", - "js-sys", - "serde", - "serde-wasm-bindgen", - "serde_json", - "serde_with", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -137,122 +78,10 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dyna3" version = "0.1.0" dependencies = [ - "charming", "console_error_panic_hook", "dyna3", "itertools", @@ -277,22 +106,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -304,28 +117,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "handlebars" -version = "6.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" -dependencies = [ - "derive_builder", - "log", - "num-order", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -336,12 +127,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "html-escape" version = "0.2.13" @@ -351,47 +136,6 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.5.0" @@ -399,8 +143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown 0.14.5", - "serde", + "hashbrown", ] [[package]] @@ -412,12 +155,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "js-sys" version = "0.3.70" @@ -455,12 +192,6 @@ dependencies = [ "rawpointer", ] -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - [[package]] name = "minicov" version = "0.3.5" @@ -517,12 +248,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-integer" version = "0.1.46" @@ -532,21 +257,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-modular" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" - -[[package]] -name = "num-order" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" -dependencies = [ - "num-modular", -] - [[package]] name = "num-rational" version = "0.4.2" @@ -579,57 +289,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pest" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.20" @@ -704,12 +363,6 @@ dependencies = [ "syn", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "safe_arch" version = "0.7.2" @@ -734,90 +387,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-wasm-bindgen" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" -dependencies = [ - "base64", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.5.0", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -852,20 +421,14 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "sycamore" -version = "0.9.1" +version = "0.9.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" +checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" dependencies = [ - "hashbrown 0.14.5", - "indexmap 2.5.0", + "hashbrown", + "indexmap", "paste", "sycamore-core", "sycamore-macro", @@ -877,20 +440,20 @@ dependencies = [ [[package]] name = "sycamore-core" -version = "0.9.1" +version = "0.9.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" +checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" dependencies = [ - "hashbrown 0.14.5", + "hashbrown", "paste", "sycamore-reactive", ] [[package]] name = "sycamore-macro" -version = "0.9.1" +version = "0.9.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137" +checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" dependencies = [ "once_cell", "proc-macro2", @@ -902,21 +465,20 @@ dependencies = [ [[package]] name = "sycamore-reactive" -version = "0.9.1" +version = "0.9.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add" +checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" dependencies = [ "paste", "slotmap", "smallvec", - "wasm-bindgen", ] [[package]] name = "sycamore-view-parser" -version = "0.9.1" +version = "0.9.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033" +checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" dependencies = [ "proc-macro2", "quote", @@ -925,9 +487,9 @@ dependencies = [ [[package]] name = "sycamore-web" -version = "0.9.1" +version = "0.9.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd" +checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" dependencies = [ "html-escape", "js-sys", @@ -943,78 +505,21 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "unicode-ident" version = "1.0.13" @@ -1171,65 +676,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 1230b47..844a0a6 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -15,10 +15,7 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -sycamore = "0.9.1" - -# We use Charming to help display engine diagnostics -charming = { version = "0.5.1", features = ["wasm"] } +sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires @@ -49,13 +46,6 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" -# turn off spurious warnings about the custom config that Sycamore uses -# -# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr -# -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] } - [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols diff --git a/app-proto/Trunk.toml b/app-proto/Trunk.toml deleted file mode 100644 index 017deba..0000000 --- a/app-proto/Trunk.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -public_url = "./" \ No newline at end of file diff --git a/app-proto/examples/common/print.rs b/app-proto/examples/common/print.rs deleted file mode 100644 index 2aa6a39..0000000 --- a/app-proto/examples/common/print.rs +++ /dev/null @@ -1,36 +0,0 @@ -#![allow(dead_code)] - -use nalgebra::DMatrix; - -use dyna3::engine::{Q, DescentHistory, Realization}; - -pub fn title(title: &str) { - println!("─── {title} ───"); -} - -pub fn realization_diagnostics(realization: &Realization) { - let Realization { result, history } = realization; - println!(); - if let Err(ref message) = result { - println!("āŒļø {message}"); - } else { - println!("āœ…ļø Target accuracy achieved!"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); -} - -pub fn gram_matrix(config: &DMatrix) { - println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); -} - -pub fn config(config: &DMatrix) { - println!("\nConfiguration:{}", config.to_string().trim_end()); -} - -pub fn loss_history(history: &DescentHistory) { - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } -} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 0d710ff..639a494 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,23 +1,25 @@ -#[path = "common/print.rs"] -mod print; - -use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet}; +use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let realization = realize_irisawa_hexlet(SCALED_TOL); - print::title("Irisawa hexlet"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, .. }) = realization.result { - // print the diameters of the chain spheres + let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + if success { println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } - - // print the completed Gram matrix - print::gram_matrix(&config); } - print::loss_history(&realization.history); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index ae4eb07..2779ab1 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,32 +1,30 @@ -#[path = "common/print.rs"] -mod print; - use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; +use dyna3::engine::{Q, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let realization = realize_kaleidocycle(SCALED_TOL); - print::title("Kaleidocycle"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { - // print the completed Gram matrix and the realized configuration - print::gram_matrix(&config); - print::config(&config); - - // find the kaleidocycle's twist motion by projecting onto the tangent - // space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1), - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); + print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}\n", history.scaled_loss.last().unwrap()); + + // find the kaleidocycle's twist motion by projecting onto the tangent space + const N_POINTS: usize = 12; + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + print!("Twist motion:{}", normalization * twist_motion); } \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index a73490e..880d7b0 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,13 +1,4 @@ -#[path = "common/print.rs"] -mod print; - -use dyna3::engine::{ - point, - realize_gram, - sphere, - ConfigNeighborhood, - ConstraintProblem, -}; +use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; fn main() { let mut problem = ConstraintProblem::from_guess(&[ @@ -20,14 +11,21 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - let realization = realize_gram( + println!(); + let (config, _, success, history) = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print::title("Point on a sphere"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, .. }) = realization.result { - print::gram_matrix(&config); - print::config(&config); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); } - print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 7901e31..3f3cc44 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,12 +1,4 @@ -#[path = "common/print.rs"] -mod print; - -use dyna3::engine::{ - realize_gram, - sphere, - ConfigNeighborhood, - ConstraintProblem, -}; +use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -14,7 +6,7 @@ fn main() { &[ sphere(1.0, 0.0, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0), - sphere(-0.5, -a, 0.0, 1.0), + sphere(-0.5, -a, 0.0, 1.0) ] }); for j in 0..3 { @@ -22,13 +14,20 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - let realization = realize_gram( + println!(); + let (config, _, success, history) = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print::title("Three spheres"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, .. }) = realization.result { - print::gram_matrix(&config); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); } - print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/index.html b/app-proto/index.html index 4fbe52f..92238f4 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,12 +6,6 @@ - - - diff --git a/app-proto/main.css b/app-proto/main.css index 7981285..f787535 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -18,17 +18,6 @@ body { font-family: 'Fira Sans', sans-serif; } -.invalid { - color: var(--text-invalid); -} - -.status { - width: 20px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - /* sidebar */ #sidebar { @@ -101,10 +90,6 @@ summary > div, .regulator { padding-right: 8px; } -.element > input { - margin-left: 8px; -} - .element-switch { width: 18px; padding-left: 2px; @@ -149,7 +134,6 @@ details[open]:has(li) .element-switch::after { } .regulator-input { - margin-right: 4px; color: inherit; background-color: inherit; border: 1px solid var(--border); @@ -171,56 +155,22 @@ details[open]:has(li) .element-switch::after { border-color: var(--border-invalid); } +.status { + width: 20px; + padding-left: 4px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } -/* diagnostics */ - -#diagnostics { - margin: 10px; -} - -#diagnostics-bar { - display: flex; -} - -#realization-status { - display: flex; - flex-grow: 1; -} - -#realization-status .status { - margin-right: 4px; -} - -#realization-status :not(.status) { - flex-grow: 1; -} - -#realization-status .status::after { - content: 'āœ“'; -} - -#realization-status.invalid .status::after { - content: '⚠'; -} - -.diagnostics-panel { - margin-top: 10px; - min-height: 180px; -} - -.diagnostics-chart { - background-color: var(--display-background); - border: 1px solid var(--border); - border-radius: 8px; -} - /* display */ -#display { +canvas { float: left; margin-left: 20px; margin-top: 20px; @@ -229,7 +179,7 @@ details[open]:has(li) .element-switch::after { border-radius: 16px; } -#display:focus { +canvas:focus { border-color: var(--border-focus-dark); outline: none; } diff --git a/app-proto/run-examples b/app-proto/run-examples new file mode 100755 index 0000000..52173b0 --- /dev/null +++ b/app-proto/run-examples @@ -0,0 +1,12 @@ +#!/bin/sh + +# run all Cargo examples, as described here: +# +# Karol Kuczmarski. "Add examples to your Rust libraries" +# http://xion.io/post/code/rust-examples.html +# + +cargo run --example irisawa-hexlet +cargo run --example three-spheres +cargo run --example point-on-sphere +cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs new file mode 100644 index 0000000..f3bbc97 --- /dev/null +++ b/app-proto/src/add_remove.rs @@ -0,0 +1,257 @@ +use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc}; +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{ + engine, + AppState, + assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} +}; + +/* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) + ); +} + +/* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system +fn load_low_curv_assemb(assembly: &Assembly) { + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Sphere::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); +} + +fn load_pointed_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Point::new( + format!("point_front"), + format!("Front point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + 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}"), + format!("Sphere {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::sphere(x, y, 0.0, 1.0) + ) + ); + + let _ = assembly.try_insert_element( + Point::new( + format!("point{index_x}{index_y}"), + format!("Point {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::point(x, y, 0.0) + ) + ); + } + } +} + +#[component] +pub fn AddRemove() -> View { + /* DEBUG */ + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + 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()); + state.selection.update(|sel| sel.clear()); + + // load assembly + match name.as_str() { + "general" => load_gen_assemb(assembly), + "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), + _ => () + }; + }); + }); + + view! { + div(id="add-remove") { + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add sphere" } + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add point" } + button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() + ); + state.assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(subjects)) + ); + state.selection.update(|sel| sel.clear()); + } + ) { "šŸ”—" } + select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + option(value="pointed") { "Pointed" } + option(value="empty") { "Empty" } + } + } + } +} \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 94e7b3c..bd185c8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,34 +1,32 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ + any::{Any, TypeId}, cell::Cell, - cmp::Ordering, collections::{BTreeMap, BTreeSet}, + cmp::Ordering, fmt, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, rc::Rc, - sync::{atomic, atomic::AtomicU64}, + sync::{atomic, atomic::AtomicU64} }; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - components::{display::DisplayItem, outline::OutlineItem}, + display::DisplayItem, engine::{ Q, + change_half_curvature, local_unif_to_std, point, - project_point_to_normalized, - project_sphere_to_normalized, realize_gram, sphere, - ConfigNeighborhood, ConfigSubspace, - ConstraintProblem, - DescentHistory, - Realization, + ConstraintProblem }, - specified::SpecifiedValue, + outline::OutlineItem, + specified::SpecifiedValue }; pub type ElementColor = [f32; 3]; @@ -103,16 +101,11 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { 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 @@ -161,10 +154,9 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub ghost: Signal, pub regulators: Signal>>, serial: u64, - column_index: Cell>, + column_index: Cell> } impl Sphere { @@ -174,17 +166,16 @@ impl Sphere { id: String, label: String, color: ElementColor, - representation: DVector, - ) -> Self { - Self { - id, - label, - color, + representation: DVector + ) -> Sphere { + Sphere { + id: id, + label: label, + color: color, representation: create_signal(representation), - ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), - column_index: None.into(), + column_index: None.into() } } } @@ -194,12 +185,12 @@ impl Element for Sphere { "sphere".to_string() } - fn default(id: String, id_num: u64) -> Self { - Self::new( + fn default(id: String, id_num: u64) -> Sphere { + Sphere::new( id, format!("Sphere {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], - sphere(0.0, 0.0, 0.0, 1.0), + sphere(0.0, 0.0, 0.0, 1.0) ) } @@ -219,18 +210,10 @@ impl Element for Sphere { 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() } @@ -261,10 +244,9 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub ghost: Signal, pub regulators: Signal>>, serial: u64, - column_index: Cell>, + column_index: Cell> } impl Point { @@ -274,17 +256,16 @@ impl Point { id: String, label: String, color: ElementColor, - representation: DVector, - ) -> Self { - Self { + representation: DVector + ) -> Point { + Point { id, label, color, representation: create_signal(representation), - ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), - column_index: None.into(), + column_index: None.into() } } } @@ -294,12 +275,12 @@ impl Element for Point { "point".to_string() } - fn default(id: String, id_num: u64) -> Self { - Self::new( + fn default(id: String, id_num: u64) -> Point { + Point::new( id, format!("Point {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], - point(0.0, 0.0, 0.0), + point(0.0, 0.0, 0.0) ) } @@ -315,18 +296,10 @@ impl Element for Point { 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() } @@ -348,7 +321,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(Self::WEIGHT_COMPONENT, index, 0.5); + problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5); problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } @@ -357,6 +330,16 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem { fn subjects(&self) -> Vec>; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; + + // this method is used to responsively precondition the assembly for + // realization when the regulator becomes a constraint, or is edited while + // acting as a constraint. it should track the set point, do any desired + // preconditioning when the set point is present, and use its return value + // to report whether the set is present. the default implementation does no + // preconditioning + fn try_activate(&self) -> bool { + self.set_point().with(|set_pt| set_pt.is_present()) + } } impl Hash for dyn Regulator { @@ -389,11 +372,11 @@ pub struct InversiveDistanceRegulator { pub subjects: [Rc; 2], pub measurement: ReadSignal, pub set_point: Signal, - serial: u64, + serial: u64 } impl InversiveDistanceRegulator { - pub fn new(subjects: [Rc; 2]) -> Self { + pub fn new(subjects: [Rc; 2]) -> InversiveDistanceRegulator { let representations = subjects.each_ref().map(|subj| subj.representation()); let measurement = create_memo(move || { representations[0].with(|rep_0| @@ -406,7 +389,7 @@ impl InversiveDistanceRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - Self { subjects, measurement, set_point, serial } + InversiveDistanceRegulator { subjects, measurement, set_point, serial } } } @@ -449,11 +432,11 @@ pub struct HalfCurvatureRegulator { pub subject: Rc, pub measurement: ReadSignal, pub set_point: Signal, - serial: u64, + serial: u64 } impl HalfCurvatureRegulator { - pub fn new(subject: Rc) -> Self { + pub fn new(subject: Rc) -> HalfCurvatureRegulator { let measurement = subject.representation().map( |rep| rep[Sphere::CURVATURE_COMPONENT] ); @@ -461,7 +444,7 @@ impl HalfCurvatureRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - Self { subject, measurement, set_point, serial } + HalfCurvatureRegulator { subject, measurement, set_point, serial } } } @@ -477,6 +460,18 @@ impl Regulator for HalfCurvatureRegulator { fn set_point(&self) -> Signal { self.set_point } + + fn try_activate(&self) -> bool { + match self.set_point.with(|set_pt| set_pt.value) { + Some(half_curv) => { + self.subject.representation().update( + |rep| change_half_curvature(rep, half_curv) + ); + true + } + None => false + } + } } impl Serial for HalfCurvatureRegulator { @@ -501,7 +496,7 @@ impl ProblemPoser for HalfCurvatureRegulator { // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub element: Rc, - pub velocity: DVectorView<'a, f64>, + pub velocity: DVectorView<'a, f64> } type AssemblyMotion<'a> = Vec>; @@ -526,44 +521,17 @@ pub struct Assembly { 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, + pub elements_by_id: Signal>> } impl Assembly { pub fn new() -> Assembly { - // create an assembly - let assembly = Assembly { + Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()), - realization_trigger: create_signal(()), - realization_status: create_signal(Ok(())), - descent_history: create_signal(DescentHistory::new()), - }; - - // 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(); - create_effect(move || { - assembly_for_effect.elements.track(); - assembly_for_effect.regulators.with( - |regs| for reg in regs { - reg.set_point().track(); - } - ); - assembly_for_effect.realization_trigger.track(); - assembly_for_effect.realize(); - }); - - assembly + elements_by_id: create_signal(BTreeMap::default()) + } } // --- inserting elements and regulators --- @@ -624,12 +592,27 @@ impl Assembly { regulators.update(|regs| regs.insert(regulator.clone())); } + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + let self_for_effect = self.clone(); + create_effect(move || { + /* DEBUG */ + // log the regulator update + console::log_1(&JsValue::from( + format!("Updated regulator with subjects {:?}", regulator.subjects()) + )); + + if regulator.try_activate() { + self_for_effect.realize(); + } + }); + /* DEBUG */ // print an updated list of regulators - console_log!("Regulators:"); + console::log_1(&JsValue::from("Regulators:")); self.regulators.with_untracked(|regs| { for reg in regs.into_iter() { - console_log!( + console::log_1(&JsValue::from(format!( " {:?}: {}", reg.subjects(), reg.set_point().with_untracked( @@ -642,7 +625,7 @@ impl Assembly { } } ) - ); + ))); } }); } @@ -673,58 +656,48 @@ impl Assembly { /* DEBUG */ // log the Gram matrix - console_log!("Gram matrix:\n{}", problem.gram); + console::log_1(&JsValue::from("Gram matrix:")); + problem.gram.log_to_console(); /* DEBUG */ // log the initial configuration matrix - console_log!("Old configuration:{:>8.3}", problem.guess); + console::log_1(&JsValue::from("Old configuration:")); + for j in 0..problem.guess.nrows() { + let mut row_str = String::new(); + for k in 0..problem.guess.ncols() { + row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); + } + console::log_1(&JsValue::from(row_str)); + } // look for a configuration with the given Gram matrix - let Realization { result, history } = realize_gram( + let (config, tangent, success, 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 { - console_log!("āŒļø {message}"); - } else { - console_log!("āœ…ļø Target accuracy achieved!"); - } - if history.scaled_loss.len() > 0 { - console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - } + // report the outcome of the search + console::log_1(&JsValue::from( + if success { + "Target accuracy achieved!" + } else { + "Failed to reach target accuracy" + } + )); + console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); + console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); + console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim())); - // report the loss history - self.descent_history.set(history); - - match result { - Ok(ConfigNeighborhood { config, nbhd: tangent }) => { - /* DEBUG */ - // report the tangent dimension - console_log!("Tangent dimension: {}", tangent.dim()); - - // 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())) - ); - } - - // save the tangent space - self.tangent.set_silent(tangent); - }, - Err(message) => { - // report the realization status. the `Err(message)` we're - // 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)) - }, + if success { + // 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())) + ); + } + + // save the tangent space + self.tangent.set_silent(tangent); } } @@ -796,26 +769,38 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward + /* KLUDGE */ + // for now, we only restore the normalizations of spheres for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { - // step the element along the deformation and then - // restore its normalization + // step the assembly along the deformation *rep += motion_proj.column(column_index); - elt.project_to_normalized(rep); + + if elt.type_id() == TypeId::of::() { + // restore normalization by contracting toward the + // last coordinate axis + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); + } }, None => { - console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) - }, + console::log_1(&JsValue::from( + format!("No velocity to unpack for fresh element \"{}\"", elt.id()) + )) + } }; }); } - // 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 - self.realization_trigger.set(()); + // bring the configuration back onto the solution variety. this also + // gets the elements' column indices and the saved tangent space back in + // sync + self.realize(); } } @@ -823,8 +808,6 @@ impl Assembly { mod tests { use super::*; - use crate::engine; - #[test] #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { @@ -850,50 +833,4 @@ mod tests { }.pose(&mut ConstraintProblem::new(2)); }); } - - #[test] - fn curvature_drift_test() { - const INITIAL_RADIUS: f64 = 0.25; - let _ = create_root(|| { - // set up an assembly containing a single sphere centered at the - // origin - let assembly = Assembly::new(); - let sphere_id = "sphere0"; - let _ = assembly.try_insert_element( - // we create the sphere by hand for two reasons: to choose the - // curvature (which can affect drift rate) and to make the test - // independent of `Sphere::default` - Sphere::new( - String::from(sphere_id), - String::from("Sphere 0"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS), - ) - ); - - // nudge the sphere repeatedly along the `z` axis - const STEP_SIZE: f64 = 0.0025; - const STEP_CNT: usize = 400; - let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); - let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); - for _ in 0..STEP_CNT { - assembly.deform( - vec![ - ElementMotion { - element: sphere.clone(), - velocity: velocity.as_view(), - } - ] - ); - } - - // check how much the sphere's curvature has drifted - const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; - const DRIFT_TOL: f64 = 0.015; - let final_half_curv = sphere.representation().with_untracked( - |rep| rep[Sphere::CURVATURE_COMPONENT] - ); - assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); - }); - } } \ No newline at end of file diff --git a/app-proto/src/components.rs b/app-proto/src/components.rs deleted file mode 100644 index 7387d58..0000000 --- a/app-proto/src/components.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod add_remove; -pub mod diagnostics; -pub mod display; -pub mod outline; -pub mod test_assembly_chooser; \ No newline at end of file diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs deleted file mode 100644 index 4196640..0000000 --- a/app-proto/src/components/add_remove.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::rc::Rc; -use sycamore::prelude::*; - -use super::test_assembly_chooser::TestAssemblyChooser; -use crate::{ - AppState, - assembly::{InversiveDistanceRegulator, Point, Sphere}, -}; - -#[component] -pub fn AddRemove() -> View { - view! { - div(id = "add-remove") { - button( - on:click = |_| { - let state = use_context::(); - batch(|| { - // this call is batched to avoid redundant realizations. - // it updates the element list and the regulator list, - // which are both tracked by the realization effect - /* TO DO */ - // it would make more to do the batching inside - // `insert_element_default`, but that will have to wait - // until Sycamore handles nested batches correctly. - // - // https://github.com/sycamore-rs/sycamore/issues/802 - // - // the nested batch issue is relevant here because the - // assembly loaders in the test assembly chooser use - // `insert_element_default` within larger batches - state.assembly.insert_element_default::(); - }); - } - ) { "Add sphere" } - button( - on:click = |_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "Add point" } - button( - class = "emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button - disabled = { - let state = use_context::(); - state.selection.with(|sel| sel.len() != 2) - }, - on:click = |_| { - let state = use_context::(); - let subjects: [_; 2] = state.selection.with( - // the button is only enabled when two elements are - // selected, so we know the cast to a two-element array - // will succeed - |sel| sel - .clone() - .into_iter() - .collect::>() - .try_into() - .unwrap() - ); - state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects)) - ); - state.selection.update(|sel| sel.clear()); - } - ) { "šŸ”—" } - TestAssemblyChooser {} - } - } -} \ No newline at end of file diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs deleted file mode 100644 index e265982..0000000 --- a/app-proto/src/components/diagnostics.rs +++ /dev/null @@ -1,256 +0,0 @@ -use charming::{ - Chart, - WasmRenderer, - component::{Axis, DataZoom, Grid}, - element::{AxisType, Symbol}, - series::{Line, Scatter}, -}; -use sycamore::prelude::*; - -use crate::AppState; - -#[derive(Clone)] -struct DiagnosticsState { - active_tab: Signal, -} - -impl DiagnosticsState { - fn new(initial_tab: String) -> Self { - Self { active_tab: create_signal(initial_tab) } - } -} - -// a realization status indicator -#[component] -fn RealizationStatus() -> View { - let state = use_context::(); - let realization_status = state.assembly.realization_status; - view! { - div( - id = "realization-status", - class = realization_status.with( - |status| match status { - Ok(_) => "", - Err(_) => "invalid", - } - ) - ) { - div(class = "status") - div { - (realization_status.with( - |status| match status { - Ok(_) => "Target accuracy achieved".to_string(), - Err(message) => message.clone(), - } - )) - } - } - } -} - -fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { - vec![ - Some(step as f64), - if value == 0.0 { None } else { Some(value.abs().log10()) }, - ] -} - -// the loss history from the last realization -#[component] -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 - let scaled_loss: Vec<_> = state.assembly.descent_history.with( - |history| history.scaled_loss - .iter() - .enumerate() - .map(|(step, &loss)| (step, loss)) - .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 - // chart: it instead leads to previous data being re-used - let scaled_loss_series = Line::new().data( - if scaled_loss.len() > 0 { - scaled_loss - } else { - vec![vec![Some(0.0), None::]] - } - ); - let chart = Chart::new() - .animation(false) - .data_zoom(DataZoom::new().y_axis_index(0).right(40)) - .x_axis(step_axis) - .y_axis(scaled_loss_axis) - .grid(Grid::new().top(20).right(80).bottom(30).left(60)) - .series(scaled_loss_series); - renderer.render(CONTAINER_ID, &chart).unwrap(); - }); - }); - - view! { - div(id = CONTAINER_ID, class = "diagnostics-chart") - } -} - -// the spectrum of the Hessian during the last realization -#[component] -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 - // positive, negative, and strictly-zero parts - let ( - hess_eigvals_zero, - hess_eigvals_nonzero, - ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( - |history| history.hess_eigvals - .iter() - .enumerate() - .map( - |(step, eigvals)| eigvals.iter().map( - move |&val| (step, val) - ) - ) - .flatten() - .partition(|&(_, val)| val == 0.0) - ); - let zero_level = hess_eigvals_nonzero - .iter() - .map(|(_, val)| val.abs()) - .reduce(f64::min) - .map(|val| 0.1 * val) - .unwrap_or(1.0); - let ( - hess_eigvals_pos, - hess_eigvals_neg, - ): (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 - // chart: it instead leads to previous data being re-used - let eigval_series_pos = Scatter::new() - .symbol_size(4.5) - .data( - if hess_eigvals_pos.len() > 0 { - hess_eigvals_pos - .into_iter() - .map(into_log10_time_point) - .collect() - } else { - vec![vec![Some(0.0), None::]] - } - ); - let eigval_series_neg = Scatter::new() - .symbol(Symbol::Diamond) - .symbol_size(6.0) - .data( - if hess_eigvals_neg.len() > 0 { - hess_eigvals_neg - .into_iter() - .map(into_log10_time_point) - .collect() - } else { - vec![vec![Some(0.0), None::]] - } - ); - let eigval_series_zero = Scatter::new() - .symbol(Symbol::Triangle) - .symbol_size(5.0) - .data( - if hess_eigvals_zero.len() > 0 { - hess_eigvals_zero - .into_iter() - .map(|(step, _)| (step, zero_level)) - .map(into_log10_time_point) - .collect() - } else { - vec![vec![Some(0.0), None::]] - } - ); - let chart = Chart::new() - .animation(false) - .data_zoom(DataZoom::new().y_axis_index(0).right(40)) - .x_axis(step_axis) - .y_axis(eigval_axis) - .grid(Grid::new().top(20).right(80).bottom(30).left(60)) - .series(eigval_series_pos) - .series(eigval_series_neg) - .series(eigval_series_zero); - renderer.render(CONTAINER_ID, &chart).unwrap(); - }); - }); - - view! { - div(id = CONTAINER_ID, class = "diagnostics-chart") - } -} - -#[component(inline_props)] -fn DiagnosticsPanel(name: &'static str, children: Children) -> View { - let diagnostics_state = use_context::(); - view! { - div( - class = "diagnostics-panel", - "hidden" = diagnostics_state.active_tab.with( - |active_tab| { - if active_tab == name { - None - } else { - Some("") - } - } - ) - ) { - (children) - } - } -} - -#[component] -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") { - RealizationStatus {} - select(bind:value = active_tab) { - option(value = "loss") { "Loss" } - option(value = "spectrum") { "Spectrum" } - } - } - DiagnosticsPanel(name = "loss") { LossHistory {} } - DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } - } - } -} \ 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 deleted file mode 100644 index 0d387d3..0000000 --- a/app-proto/src/components/test_assembly_chooser.rs +++ /dev/null @@ -1,941 +0,0 @@ -use itertools::izip; -use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc}; -use nalgebra::Vector3; -use sycamore::prelude::*; -use web_sys::{console, wasm_bindgen::JsValue}; - -use crate::{ - AppState, - assembly::{ - Assembly, - Element, - ElementColor, - InversiveDistanceRegulator, - Point, - Sphere, - }, - engine, - engine::DescentHistory, - specified::SpecifiedValue, -}; - -// --- loaders --- - -/* DEBUG */ -// each of these functions loads an example assembly for testing. once we've -// done more work on saving and loading assemblies, we should come back to this -// code to see if it can be simplified - -fn load_general(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_a"), - String::from("Castor"), - [1.00_f32, 0.25_f32, 0.00_f32], - engine::sphere(0.5, 0.5, 0.0, 1.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_b"), - String::from("Pollux"), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(-0.5, -0.5, 0.0, 1.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_major"), - String::from("Ursa major"), - [0.25_f32, 0.00_f32, 1.00_f32], - engine::sphere(-0.5, 0.5, 0.0, 0.75), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_minor"), - String::from("Ursa minor"), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere(0.5, -0.5, 0.0, 0.5), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_deimos"), - String::from("Deimos"), - [0.75_f32, 0.75_f32, 0.00_f32], - engine::sphere(0.0, 0.15, 1.0, 0.25), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_phobos"), - String::from("Phobos"), - [0.00_f32, 0.75_f32, 0.50_f32], - engine::sphere(0.0, -0.15, -1.0, 0.25), - ) - ); -} - -fn load_low_curvature(assembly: &Assembly) { - // create the spheres - let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( - Sphere::new( - "central".to_string(), - "Central".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "assemb_plane".to_string(), - "Assembly plane".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side1".to_string(), - "Side 1".to_string(), - [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side2".to_string(), - "Side 2".to_string(), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side3".to_string(), - "Side 3".to_string(), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner1".to_string(), - "Corner 1".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner2".to_string(), - "Corner 2".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("corner3"), - String::from("Corner 3"), - [0.75_f32, 0.75_f32, 0.75_f32], - 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( - |id| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[id].clone() - ) - ); - let sides = index_range.clone().map( - |k| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("side{k}")].clone() - ) - ); - let corners = index_range.map( - |k| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("corner{k}")].clone() - ) - ); - for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) { - // fix the curvature of each plane - let curvature = plane.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - curvature.set_point().set(SpecifiedValue::try_from("0".to_string()).unwrap()); - } - let all_perpendicular = [central.clone()].into_iter() - .chain(sides.clone()) - .chain(corners.clone()); - for sphere in all_perpendicular { - // make each side and packed sphere perpendicular to the assembly plane - let right_angle = InversiveDistanceRegulator::new([sphere, assemb_plane.clone()]); - right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); - assembly.insert_regulator(Rc::new(right_angle)); - } - for sphere in sides.clone().chain(corners.clone()) { - // make each side and corner sphere tangent to the central sphere - let tangency = InversiveDistanceRegulator::new([sphere.clone(), central.clone()]); - tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); - assembly.insert_regulator(Rc::new(tangency)); - } - for (side_index, side) in sides.enumerate() { - // make each side tangent to the two adjacent corner spheres - for (corner_index, corner) in corners.clone().enumerate() { - if side_index != corner_index { - let tangency = InversiveDistanceRegulator::new([side.clone(), corner]); - tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); - assembly.insert_regulator(Rc::new(tangency)); - } - } - } -} - -fn load_pointed(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Point::new( - format!("point_front"), - format!("Front point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, FRAC_1_SQRT_2), - ) - ); - let _ = assembly.try_insert_element( - Point::new( - format!("point_back"), - format!("Back point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, -FRAC_1_SQRT_2), - ) - ); - for index_x in 0..=1 { - 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}"), - format!("Sphere {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::sphere(x, y, 0.0, 1.0), - ) - ); - - let _ = assembly.try_insert_element( - Point::new( - format!("point{index_x}{index_y}"), - format!("Point {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::point(x, y, 0.0), - ) - ); - } - } -} - -// to finish describing the tridiminished icosahedron, set the inversive -// distance regulators as follows: -// A-A -0.25 -// A-B " -// B-C " -// C-C " -// A-C -0.25 * φ^2 = -0.6545084971874737 -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]; - const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32]; - let vertices = [ - Point::new( - "a1".to_string(), - "A₁".to_string(), - COLOR_A, - engine::point(0.25, 0.75, 0.75), - ), - Point::new( - "a2".to_string(), - "Aā‚‚".to_string(), - COLOR_A, - engine::point(0.75, 0.25, 0.75), - ), - Point::new( - "a3".to_string(), - "Aā‚ƒ".to_string(), - COLOR_A, - engine::point(0.75, 0.75, 0.25), - ), - Point::new( - "b1".to_string(), - "B₁".to_string(), - COLOR_B, - engine::point(0.75, -0.25, -0.25), - ), - Point::new( - "b2".to_string(), - "Bā‚‚".to_string(), - COLOR_B, - engine::point(-0.25, 0.75, -0.25), - ), - Point::new( - "b3".to_string(), - "Bā‚ƒ".to_string(), - COLOR_B, - engine::point(-0.25, -0.25, 0.75), - ), - Point::new( - "c1".to_string(), - "C₁".to_string(), - COLOR_C, - engine::point(0.0, -1.0, -1.0), - ), - Point::new( - "c2".to_string(), - "Cā‚‚".to_string(), - COLOR_C, - engine::point(-1.0, 0.0, -1.0), - ), - Point::new( - "c3".to_string(), - "Cā‚ƒ".to_string(), - COLOR_C, - engine::point(-1.0, -1.0, 0.0), - ), - ]; - 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(); - let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6; - let faces = [ - Sphere::new( - "face1".to_string(), - "Face 1".to_string(), - COLOR_FACE, - engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), - ), - Sphere::new( - "face2".to_string(), - "Face 2".to_string(), - COLOR_FACE, - engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), - ), - Sphere::new( - "face3".to_string(), - "Face 3".to_string(), - COLOR_FACE, - engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0), - ), - ]; - for face in faces { - 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 - let face = assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("face{j}")].clone() - ); - let curvature_regulator = face.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - 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() - ); - 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( - |elts_by_id| elts_by_id[&format!("{series}{j}")].clone() - ) - ); - 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 - let vertex = assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("{series}{k}")].clone() - ); - 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( - |index| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("{series}{index}")].clone() - ) - ) - ); - for pair in adjacent_pairs { - assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(pair)) - ); - } - } -} - -// to finish describing the dodecahedral circle packing, set the inversive -// distance regulators to -1. some of the regulators have already been set -fn load_dodecahedral_packing(assembly: &Assembly) { - // add the substrate - let _ = assembly.try_insert_element( - Sphere::new( - "substrate".to_string(), - "Substrate".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0), - ) - ); - 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]; - const COLOR_C: ElementColor = [0.25_f32, 0.00_f32, 1.00_f32]; - let phi = 0.5 + 1.25_f64.sqrt(); /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized - let phi_inv = 1.0 / phi; - let coord_scale = (phi + 2.0).sqrt(); - let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale]; - let face_radii = [phi_inv, 5.0 / 12.0]; - let mut faces = Vec::>::new(); - let subscripts = ["ā‚€", "₁"]; - for j in 0..2 { - 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( - Sphere::new( - id_a.clone(), - format!("A{label_sub}"), - COLOR_A, - engine::sphere(0.0, small_coord, big_coord, face_radii[k]), - ) - ); - faces.push( - assembly.elements_by_id.with_untracked( - |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( - Sphere::new( - id_b.clone(), - format!("B{label_sub}"), - COLOR_B, - engine::sphere(small_coord, big_coord, 0.0, face_radii[k]), - ) - ); - faces.push( - assembly.elements_by_id.with_untracked( - |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( - Sphere::new( - id_c.clone(), - format!("C{label_sub}"), - COLOR_C, - engine::sphere(big_coord, 0.0, small_coord, face_radii[k]), - ) - ); - faces.push( - assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&id_c].clone() - ) - ); - } - } - - // 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 { - let long_edge_ids = [ - format!("{long_edge_plane}{k}0"), - format!("{long_edge_plane}{k}1") - ]; - let short_edge_ids = [ - format!("{short_edge_plane}0{k}"), - format!("{short_edge_plane}1{k}") - ]; - let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map( - |edge_ids| edge_ids.map( - |id| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&id].clone() - ) - ) - ); - - // 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 { - let side_tangency = InversiveDistanceRegulator::new( - [long_edge[i].clone(), short_edge[j].clone()] - ); - if i == 0 && k == 0 { - side_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); - } - assembly.insert_regulator(Rc::new(side_tangency)); - } - } - } - } -} - -// the initial configuration of this test assembly deliberately violates the -// constraints, so loading the assembly will trigger a non-trivial realization -fn load_balanced(assembly: &Assembly) { - // create the spheres - const R_OUTER: f64 = 10.0; - const R_INNER: f64 = 4.0; - let spheres = [ - Sphere::new( - "outer".to_string(), - "Outer".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, R_OUTER), - ), - Sphere::new( - "a".to_string(), - "A".to_string(), - [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere(0.0, 4.0, 0.0, R_INNER), - ), - Sphere::new( - "b".to_string(), - "B".to_string(), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(0.0, -4.0, 0.0, R_INNER), - ), - ]; - 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), - (a.clone(), R_INNER), - (b.clone(), R_INNER), - ] { - let curvature_regulator = sphere.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - let curvature = 0.5 / radius; - curvature_regulator.set_point().set( - 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] { - let tangency = InversiveDistanceRegulator::new([outer.clone(), inner]); - tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); - assembly.insert_regulator(Rc::new(tangency)); - } -} - -// 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(assembly: &Assembly) { - // create a point almost at the origin and a sphere centered on the origin - let _ = assembly.try_insert_element( - Point::new( - "point".to_string(), - "Point".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(1e-9, 0.0, 0.0), - ), - ); - let _ = assembly.try_insert_element( - Sphere::new( - "sphere".to_string(), - "Sphere".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - 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()); - assembly.insert_regulator(Rc::new(incidence)); -} - -// setting the inversive distances between the vertices to -2 gives a regular -// tetrahedron with side length 1, whose insphere and circumsphere have radii -// 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(assembly: &Assembly) { - let index_range = 1..=4; - - // create the spheres - const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; - let spheres = [ - Sphere::new( - "sphere_faces".to_string(), - "Insphere".to_string(), - GRAY, - engine::sphere(0.0, 0.0, 0.0, 0.5), - ), - Sphere::new( - "sphere_vertices".to_string(), - "Circumsphere".to_string(), - GRAY, - engine::sphere(0.0, 0.0, 0.0, 0.25), - ), - ]; - for sphere in spheres { - let _ = assembly.try_insert_element(sphere); - } - - // create the vertices - let vertices = izip!( - index_range.clone(), - [ - [1.00_f32, 0.50_f32, 0.75_f32], - [1.00_f32, 0.75_f32, 0.50_f32], - [1.00_f32, 1.00_f32, 0.50_f32], - [0.75_f32, 0.50_f32, 1.00_f32], - ].into_iter(), - [ - engine::point(-0.6, -0.8, -0.6), - engine::point(-0.6, 0.8, 0.6), - engine::point(0.6, -0.8, 0.6), - engine::point(0.6, 0.8, -0.6), - ].into_iter() - ).map( - |(k, color, representation)| { - Point::new( - format!("v{k}"), - format!("Vertex {k}"), - color, - representation, - ) - } - ); - 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)); - let faces = izip!( - index_range.clone(), - [ - [1.00_f32, 0.00_f32, 0.25_f32], - [1.00_f32, 0.25_f32, 0.00_f32], - [0.75_f32, 0.75_f32, 0.00_f32], - [0.25_f32, 0.00_f32, 1.00_f32], - ].into_iter(), - [ - engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0), - engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), - engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), - engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0), - ].into_iter() - ).map( - |(k, color, representation)| { - Sphere::new( - format!("f{k}"), - format!("Face {k}"), - color, - representation, - ) - } - ); - for face in faces { - face.ghost().set(true); - let _ = assembly.try_insert_element(face); - } - - // impose the constraints - for j in index_range.clone() { - let [face_j, vertex_j] = [ - format!("f{j}"), - format!("v{j}"), - ].map( - |id| assembly.elements_by_id.with_untracked( - |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() - ); - 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( - [vertex_j.clone(), vertex_k.clone()] - ); - 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()); - assembly.insert_regulator(Rc::new(incidence_regulator)); - } - } -} - -// to finish setting up the problem, fix the following curvatures: -// sun 1 -// moon 5/3 = 1.666666666666666... -// chain1 2 -// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization -// failures before they happen, or resolves them after they happen. the result -// depends sensitively on the translation direction, suggesting that realization -// is failing because the engine is having trouble breaking a symmetry -// /* TO DO */ -// the engine's performance on this problem is scale-dependent! with the current -// initial conditions, realization fails for any order of imposing the remaining -// curvature constraints. scaling everything up by a factor of ten, as done in -// the original problem, makes realization succeed reliably. one potentially -// relevant difference is that a lot of the numbers in the current initial -// 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(assembly: &Assembly) { - let index_range = 1..=6; - let colors = [ - [1.00_f32, 0.00_f32, 0.25_f32], - [1.00_f32, 0.25_f32, 0.00_f32], - [0.75_f32, 0.75_f32, 0.00_f32], - [0.25_f32, 1.00_f32, 0.00_f32], - [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( - "outer".to_string(), - "Outer".to_string(), - [0.5_f32, 0.5_f32, 0.5_f32], - engine::sphere(0.0, 0.0, 0.0, 1.5), - ), - Sphere::new( - "sun".to_string(), - "Sun".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, -0.75, 0.0, 0.75), - ), - Sphere::new( - "moon".to_string(), - "Moon".to_string(), - [0.25_f32, 0.25_f32, 0.25_f32], - engine::sphere(0.0, 0.75, 0.0, 0.75), - ), - ].into_iter().chain( - index_range.clone().zip(colors).map( - |(k, color)| { - let ang = (k as f64) * PI/3.0; - Sphere::new( - format!("chain{k}"), - format!("Chain {k}"), - color, - engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5), - ) - } - ) - ); - 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() - ); - outer.ghost().set(true); - let outer_curvature_regulator = outer.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - 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( - |elts_by_id| elts_by_id[id].clone() - ) - ); - let chain = index_range.map( - |k| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("chain{k}")].clone() - ) - ); - for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) { - for (other_sphere, inversive_distance) in [ - (outer.clone(), "1"), - (sun.clone(), "-1"), - (moon.clone(), "-1"), - (chain_sphere_next.clone(), "-1"), - ] { - let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]); - tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap()); - 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)); -} - -// --- chooser --- - -/* DEBUG */ -#[component] -pub fn TestAssemblyChooser() -> View { - // create an effect that loads the selected test assembly - let assembly_name = create_signal("general".to_string()); - create_effect(move || { - // get name of chosen assembly - let name = assembly_name.get_clone(); - 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), - "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), - _ => (), - }; - }); - }); - - // build the chooser - view! { - select(bind:value = assembly_name) { - option(value = "general") { "General" } - option(value = "low-curvature") { "Low-curvature" } - option(value = "pointed") { "Pointed" } - 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" } - option(value = "irisawa-hexlet") { "Irisawa hexlet" } - option(value = "empty") { "Empty" } - } - } -} \ No newline at end of file diff --git a/app-proto/src/components/display.rs b/app-proto/src/display.rs similarity index 88% rename from app-proto/src/components/display.rs rename to app-proto/src/display.rs index da921dd..a2fe4b6 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/display.rs @@ -12,40 +12,28 @@ use web_sys::{ WebGlProgram, WebGlShader, WebGlUniformLocation, - wasm_bindgen::{JsCast, JsValue}, + wasm_bindgen::{JsCast, JsValue} }; use crate::{ AppState, - assembly::{Element, ElementColor, ElementMotion, Point, Sphere}, + assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; -// --- color --- - -const COLOR_SIZE: usize = 3; -type ColorWithOpacity = [f32; COLOR_SIZE + 1]; - -fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity { - let mut color_with_opacity = [0.0; COLOR_SIZE + 1]; - color_with_opacity[..COLOR_SIZE].copy_from_slice(&color); - color_with_opacity[COLOR_SIZE] = opacity; - color_with_opacity -} - // --- scene data --- struct SceneSpheres { representations: Vec>, - colors_with_opacity: Vec, - highlights: Vec, + colors: Vec, + highlights: Vec } impl SceneSpheres { - fn new() -> Self { - Self { + fn new() -> SceneSpheres{ + SceneSpheres { representations: Vec::new(), - colors_with_opacity: Vec::new(), - highlights: Vec::new(), + colors: Vec::new(), + highlights: Vec::new() } } @@ -53,39 +41,33 @@ impl SceneSpheres { 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, - ) { + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32) { self.representations.push(representation); - self.colors_with_opacity.push(combine_channels(color, opacity)); + self.colors.push(color); self.highlights.push(highlight); } } struct ScenePoints { representations: Vec>, - colors_with_opacity: Vec, + colors: Vec, highlights: Vec, - selections: Vec, + selections: Vec } impl ScenePoints { - fn new() -> Self { - Self { + fn new() -> ScenePoints { + ScenePoints { representations: Vec::new(), - colors_with_opacity: Vec::new(), + colors: Vec::new(), highlights: Vec::new(), - selections: Vec::new(), + selections: Vec::new() } } - fn push( - &mut self, representation: DVector, - color: ElementColor, opacity: f32, highlight: f32, selected: bool, - ) { + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32, selected: bool) { self.representations.push(representation); - self.colors_with_opacity.push(combine_channels(color, opacity)); + self.colors.push(color); self.highlights.push(highlight); self.selections.push(if selected { 1.0 } else { 0.0 }); } @@ -93,14 +75,14 @@ impl ScenePoints { pub struct Scene { spheres: SceneSpheres, - points: ScenePoints, + points: ScenePoints } impl Scene { - fn new() -> Self { - Self { + fn new() -> Scene { + Scene { spheres: SceneSpheres::new(), - points: ScenePoints::new(), + points: ScenePoints::new() } } } @@ -111,36 +93,21 @@ pub trait DisplayItem { // 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 - fn cast( - &self, - dir: Vector3, - assembly_to_world: &DMatrix, - pixel_size: f64, - ) -> Option; + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; } impl DisplayItem for Sphere { fn show(&self, scene: &mut Scene, selected: bool) { - /* SCAFFOLDING */ - const DEFAULT_OPACITY: f32 = 0.5; - const GHOST_OPACITY: f32 = 0.2; - const HIGHLIGHT: f32 = 0.2; - + const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ 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); + scene.spheres.push(representation, color, 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( - &self, - dir: Vector3, - assembly_to_world: &DMatrix, - _pixel_size: f64, - ) -> Option { + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, _pixel_size: f64) -> Option { // 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; @@ -181,24 +148,15 @@ impl DisplayItem for Sphere { impl DisplayItem for Point { fn show(&self, scene: &mut Scene, selected: bool) { - /* SCAFFOLDING */ - const GHOST_OPACITY: f32 = 0.4; - const HIGHLIGHT: f32 = 0.5; - + const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ 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); + scene.points.push(representation, color, highlight, selected); } /* SCAFFOLDING */ - fn cast( - &self, - dir: Vector3, - assembly_to_world: &DMatrix, - pixel_size: f64, - ) -> Option { + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option { let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); if rep[2] < 0.0 { // this constant should be kept synchronized with `point.frag` @@ -241,7 +199,7 @@ fn compile_shader( fn set_up_program( context: &WebGl2RenderingContext, vertex_shader_source: &str, - fragment_shader_source: &str, + fragment_shader_source: &str ) -> WebGlProgram { // compile the shaders let vertex_shader = compile_shader( @@ -281,12 +239,12 @@ fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, var_name: &str, - member_name_opt: Option<&str>, + member_name_opt: Option<&str> ) -> [Option; N] { array::from_fn(|n| { let name = match member_name_opt { Some(member_name) => format!("{var_name}[{n}].{member_name}"), - None => format!("{var_name}[{n}]"), + None => format!("{var_name}[{n}]") }; context.get_uniform_location(&program, name.as_str()) }) @@ -297,7 +255,7 @@ fn bind_to_attribute( context: &WebGl2RenderingContext, attr_index: u32, attr_size: i32, - buffer: &Option, + buffer: &Option ) { context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); context.vertex_attrib_pointer_with_i32( @@ -313,7 +271,7 @@ fn bind_to_attribute( // load the given data into a new vertex buffer object fn load_new_buffer( context: &WebGl2RenderingContext, - data: &[f32], + data: &[f32] ) -> Option { // create a buffer and bind it to ARRAY_BUFFER let buffer = context.create_buffer(); @@ -340,7 +298,7 @@ fn bind_new_buffer_to_attribute( context: &WebGl2RenderingContext, attr_index: u32, attr_size: i32, - data: &[f32], + data: &[f32] ) { let buffer = load_new_buffer(context, data); bind_to_attribute(context, attr_index, attr_size, &buffer); @@ -362,9 +320,9 @@ fn event_dir(event: &MouseEvent) -> (Vector3, f64) { Vector3::new( FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, - -1.0, + -1.0 ), - FOCAL_SLOPE * 2.0 / shortdim, + FOCAL_SLOPE * 2.0 / shortdim ) } @@ -407,7 +365,6 @@ pub fn Display() -> View { state.assembly.elements.with(|elts| { for elt in elts { elt.representation().track(); - elt.ghost().track(); } }); state.selection.track(); @@ -438,6 +395,7 @@ pub fn Display() -> View { const SHRINKING_SPEED: f64 = 0.15; // in length units per second // display parameters + const OPACITY: f32 = 0.5; /* SCAFFOLDING */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ @@ -464,14 +422,14 @@ pub fn Display() -> View { let sphere_program = set_up_program( &ctx, include_str!("identity.vert"), - include_str!("spheres.frag"), + 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"), + include_str!("point.frag") ); /* DEBUG */ @@ -488,7 +446,7 @@ pub fn Display() -> View { // capped at 1024 elements console::log_2( &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), - &JsValue::from("uniform vectors available"), + &JsValue::from("uniform vectors available") ); // find the sphere program's vertex attribute @@ -511,6 +469,7 @@ pub fn Display() -> View { ); let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); + let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); @@ -524,7 +483,7 @@ pub fn Display() -> View { // southeast triangle -1.0, -1.0, 0.0, 1.0, 1.0, 0.0, - 1.0, -1.0, 0.0, + 1.0, -1.0, 0.0 ]; let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); @@ -617,7 +576,7 @@ pub fn Display() -> View { vec![ ElementMotion { element: sel, - velocity: elt_motion.as_view(), + velocity: elt_motion.as_view() } ] ); @@ -650,7 +609,7 @@ pub fn Display() -> View { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, u, 0.0, 0.0, 2.0*u, 1.0, u*u, - 0.0, 0.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 0.0, 0.0, 1.0 ]) }; let asm_to_world = &location * &orientation; @@ -689,23 +648,24 @@ pub fn Display() -> View { let v = &sphere_reps_world[n]; ctx.uniform3fv_with_f32_array( sphere_sp_locs[n].as_ref(), - v.rows(0, 3).as_slice(), + v.rows(0, 3).as_slice() ); ctx.uniform2fv_with_f32_array( sphere_lt_locs[n].as_ref(), - v.rows(3, 2).as_slice(), + v.rows(3, 2).as_slice() ); - ctx.uniform4fv_with_f32_array( + ctx.uniform3fv_with_f32_array( sphere_color_locs[n].as_ref(), - &scene.spheres.colors_with_opacity[n], + &scene.spheres.colors[n] ); ctx.uniform1f( sphere_highlight_locs[n].as_ref(), - scene.spheres.highlights[n], + scene.spheres.highlights[n] ); } // pass the display parameters + ctx.uniform1f(opacity_loc.as_ref(), OPACITY); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); @@ -743,7 +703,7 @@ pub fn Display() -> View { // bind them to the corresponding attributes in the vertex // shader bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); - 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_color_attr, COLOR_SIZE as i32, scene.points.colors.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()); @@ -794,7 +754,7 @@ pub fn Display() -> View { "ArrowLeft" if shift => roll_ccw.set(value), "ArrowRight" => yaw_right.set(value), "ArrowLeft" => yaw_left.set(value), - _ => navigating = false, + _ => navigating = false }; if navigating { scene_changed.set(true); @@ -814,7 +774,7 @@ pub fn Display() -> View { "s" | "S" => translate_neg_y.set(value), "]" | "}" => shrink_neg.set(value), "[" | "{" => shrink_pos.set(value), - _ => manipulating = false, + _ => manipulating = false }; if manipulating { event.prevent_default(); @@ -826,12 +786,11 @@ pub fn Display() -> View { // switch back to integer-valued parameters when that becomes possible // again canvas( - ref = display, - id = "display", - width = "600", - height = "600", - tabindex = "0", - on:keydown = move |event: KeyboardEvent| { + ref=display, + width="600", + height="600", + tabindex="0", + on:keydown=move |event: KeyboardEvent| { if event.key() == "Shift" { // swap navigation inputs roll_cw.set(yaw_right.get()); @@ -857,7 +816,7 @@ pub fn Display() -> View { set_manip_signal(&event, 1.0); } }, - on:keyup = move |event: KeyboardEvent| { + on:keyup=move |event: KeyboardEvent| { if event.key() == "Shift" { // swap navigation inputs yaw_right.set(roll_cw.get()); @@ -879,7 +838,7 @@ pub fn Display() -> View { set_manip_signal(&event, 0.0); } }, - on:blur = move |_| { + on:blur=move |_| { pitch_up.set(0.0); pitch_down.set(0.0); yaw_right.set(0.0); @@ -887,16 +846,12 @@ pub fn Display() -> View { roll_ccw.set(0.0); roll_cw.set(0.0); }, - on:click = move |event: MouseEvent| { + on:click=move |event: MouseEvent| { // find the nearest element along the pointer direction let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(Rc, f64)> = None; - let tangible_elts = state.assembly.elements - .get_clone_untracked() - .into_iter() - .filter(|elt| !elt.ghost().get()); - for elt in tangible_elts { + for elt in state.assembly.elements.get_clone_untracked() { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { @@ -904,18 +859,18 @@ pub fn Display() -> View { clicked = Some((elt, depth)) } }, - None => clicked = Some((elt, depth)), - }, - None => (), + None => clicked = Some((elt, depth)) + } + None => () }; } // if we clicked something, select it match clicked { Some((elt, _)) => state.select(&elt, event.shift_key()), - None => state.selection.update(|sel| sel.clear()), + None => state.selection.update(|sel| sel.clear()) }; - }, + } ) } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index d033c01..b0fa23d 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,6 +1,6 @@ use lazy_static::lazy_static; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; -use std::fmt::{Display, Error, Formatter}; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -16,7 +16,7 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect center_y / radius, center_z / radius, 0.5 / radius, - 0.5 * (center_norm_sq / radius - radius), + 0.5 * (center_norm_sq / radius - radius) ]) } @@ -30,42 +30,61 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 norm_sp * dir_y, norm_sp * dir_z, 0.5 * curv, - off * (1.0 + 0.5 * off * curv), + off * (1.0 + 0.5 * off * curv) ]) } -// project a sphere's representation vector to the normalization variety by -// contracting toward the last coordinate axis -pub fn project_sphere_to_normalized(rep: &mut DVector) { - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; +// given a sphere's representation vector, change the sphere's half-curvature to +// `half-curv` and then restore normalization by contracting the representation +// vector toward the curvature axis +pub fn change_half_curvature(rep: &mut DVector, half_curv: f64) { + // set the sphere's half-curvature to the desired value + rep[3] = half_curv; + + // restore normalization by contracting toward the curvature axis + const SIZE_THRESHOLD: f64 = 1e-9; + let half_q_lt = -2.0 * half_curv * rep[4]; let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); -} - -// normalize a point's representation vector by scaling -pub fn project_point_to_normalized(rep: &mut DVector) { - rep.scale_mut(0.5 / rep[3]); + let mut spatial = rep.fixed_rows_mut::<3>(0); + let q_sp = spatial.norm_squared(); + if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD { + spatial.copy_from_slice( + &[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()] + ); + } else { + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + spatial.scale_mut(1.0 / scaling); + rep[4] /= scaling; + } + + /* DEBUG */ + // verify normalization + let rep_for_debug = rep.clone(); + console::log_1(&JsValue::from( + format!( + "Sphere self-product after curvature change: {}", + rep_for_debug.dot(&(&*Q * &rep_for_debug)) + ) + )); } // --- partial matrices --- pub struct MatrixEntry { index: (usize, usize), - value: f64, + value: f64 } pub struct PartialMatrix(Vec); impl PartialMatrix { - pub fn new() -> Self { - Self(Vec::::new()) + pub fn new() -> PartialMatrix { + PartialMatrix(Vec::::new()) } pub fn push(&mut self, row: usize, col: usize, value: f64) { - let Self(entries) = self; - entries.push(MatrixEntry { index: (row, col), value }); + let PartialMatrix(entries) = self; + entries.push(MatrixEntry { index: (row, col), value: value }); } pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { @@ -75,6 +94,15 @@ impl PartialMatrix { } } + /* DEBUG */ + pub fn log_to_console(&self) { + for &MatrixEntry { index: (row, col), value } in self { + console::log_1(&JsValue::from( + format!(" {} {} {}", row, col, value) + )); + } + } + fn freeze(&self, a: &DMatrix) -> DMatrix { let mut result = a.clone(); for &MatrixEntry { index, value } in self { @@ -100,21 +128,12 @@ impl PartialMatrix { } } -impl Display for PartialMatrix { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - for &MatrixEntry { index: (row, col), value } in self { - writeln!(f, " {row} {col} {value}")?; - } - Ok(()) - } -} - impl IntoIterator for PartialMatrix { type Item = MatrixEntry; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - let Self(entries) = self; + let PartialMatrix(entries) = self; entries.into_iter() } } @@ -135,26 +154,22 @@ impl<'a> IntoIterator for &'a PartialMatrix { pub struct ConfigSubspace { assembly_dim: usize, basis_std: Vec>, - basis_proj: Vec>, + basis_proj: Vec> } impl ConfigSubspace { - pub fn zero(assembly_dim: usize) -> Self { - Self { - assembly_dim, + pub fn zero(assembly_dim: usize) -> ConfigSubspace { + ConfigSubspace { + assembly_dim: assembly_dim, basis_proj: Vec::new(), - basis_std: Vec::new(), + 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` - fn symmetric_kernel( - a: DMatrix, - proj_to_std: DMatrix, - assembly_dim: usize, - ) -> Self { + fn symmetric_kernel(a: DMatrix, proj_to_std: DMatrix, assembly_dim: usize) -> ConfigSubspace { // 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 @@ -168,13 +183,20 @@ impl ConfigSubspace { ).collect::>().as_slice() ); + /* DEBUG */ + // print the eigenvalues + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + console::log_1(&JsValue::from( + format!("Eigenvalues used to find kernel:{}", eig.eigenvalues) + )); + // 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 { - assembly_dim, + ConfigSubspace { + assembly_dim: assembly_dim, basis_std: basis_std.column_iter().map( |v| Into::>::into( v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) @@ -184,7 +206,7 @@ impl ConfigSubspace { |v| Into::>::into( v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) ) - ).collect(), + ).collect() } } @@ -218,18 +240,18 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub hess_eigvals: Vec>, + pub min_eigval: Vec, pub base_step: Vec>, - pub backoff_steps: Vec, + pub backoff_steps: Vec } impl DescentHistory { - pub fn new() -> Self { - Self { + fn new() -> DescentHistory { + DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), - hess_eigvals: Vec::>::new(), + min_eigval: Vec::::new(), base_step: Vec::>::new(), backoff_steps: Vec::::new(), } @@ -245,21 +267,21 @@ pub struct ConstraintProblem { } impl ConstraintProblem { - pub fn new(element_count: usize) -> Self { + pub fn new(element_count: usize) -> ConstraintProblem { const ELEMENT_DIM: usize = 5; - Self { + ConstraintProblem { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), - guess: DMatrix::::zeros(ELEMENT_DIM, element_count), + guess: DMatrix::::zeros(ELEMENT_DIM, element_count) } } #[cfg(feature = "dev")] - pub fn from_guess(guess_columns: &[DVector]) -> Self { - Self { + pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { + ConstraintProblem { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), - guess: DMatrix::from_columns(guess_columns), + guess: DMatrix::from_columns(guess_columns) } } } @@ -273,21 +295,25 @@ lazy_static! { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, - 0.0, 0.0, 0.0, -2.0, 0.0, + 0.0, 0.0, 0.0, -2.0, 0.0 ]); } struct SearchState { config: DMatrix, err_proj: DMatrix, - loss: f64, + loss: f64 } impl SearchState { - fn from_config(gram: &PartialMatrix, config: DMatrix) -> Self { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let loss = err_proj.norm_squared(); - Self { config, err_proj, loss } + SearchState { + config: config, + err_proj: err_proj, + loss: loss + } } } @@ -314,7 +340,7 @@ pub fn local_unif_to_std(v: DVectorView) -> DMatrix { curv, 0.0, 0.0, 0.0, v[0], 0.0, curv, 0.0, 0.0, v[1], 0.0, 0.0, curv, 0.0, v[2], - 0.0, 0.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 0.0, 0.0, 1.0 ]) } else { // `v` represents a sphere. the normalization condition says that the @@ -323,7 +349,7 @@ pub fn local_unif_to_std(v: DVectorView) -> DMatrix { curv, 0.0, 0.0, 0.0, v[0], 0.0, curv, 0.0, 0.0, v[1], 0.0, 0.0, curv, 0.0, v[2], - curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0, + curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0 ]) } } @@ -336,7 +362,7 @@ fn seek_better_config( base_target_improvement: f64, min_efficiency: f64, backoff: f64, - max_backoff_steps: i32, + max_backoff_steps: i32 ) -> Option<(SearchState, i32)> { let mut rate = 1.0; for backoff_steps in 0..max_backoff_steps { @@ -351,17 +377,6 @@ fn seek_better_config( None } -// a first-order neighborhood of a configuration -pub struct ConfigNeighborhood { - pub config: DMatrix, - pub nbhd: ConfigSubspace, -} - -pub struct Realization { - pub result: Result, - pub history: DescentHistory, -} - // seek a matrix `config` that matches the partial matrix `problem.frozen` and // has `config' * Q * config` matching the partial matrix `problem.gram`. start // at `problem.guess`, set the frozen entries to their desired values, and then @@ -373,30 +388,19 @@ pub fn realize_gram( backoff: f64, reg_scale: f64, max_descent_steps: i32, - max_backoff_steps: i32, -) -> Realization { + max_backoff_steps: i32 +) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { // destructure the problem data - let ConstraintProblem { gram, guess, frozen } = problem; + 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 - let assembly_dim = guess.ncols(); - if assembly_dim == 0 { - let result = Ok( - ConfigNeighborhood { - config: guess.clone(), - nbhd: ConfigSubspace::zero(0), - } - ); - return Realization { result, history }; - } - // find the dimension of the search space let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); let total_dim = element_dim * assembly_dim; // scale the tolerance @@ -437,12 +441,11 @@ 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(); + let min_eigval = hess.symmetric_eigenvalues().min(); if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } - history.hess_eigvals.push(hess_eigvals); + history.min_eigval.push(min_eigval); // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace @@ -461,40 +464,30 @@ pub fn realize_gram( if state.loss < tol { break; } // compute the Newton step - /* TO DO */ /* - we should change our regularization to ensure that the Hessian is - is positive-definite, rather than just positive-semidefinite. ideally, - that would guarantee the success of the Cholesky decomposition--- - although we'd still need the error-handling routine in case of - numerical hiccups + we need to either handle or eliminate the case where the minimum + eigenvalue of the Hessian is zero, so the regularized Hessian is + singular. right now, this causes the Cholesky decomposition to return + `None`, leading to a panic when we unrap */ - let hess_cholesky = match hess.clone().cholesky() { - Some(cholesky) => cholesky, - None => return Realization { - result: Err("Cholesky decomposition failed".to_string()), - history, - }, - }; - let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); + let base_step_stacked = hess.clone().cholesky().unwrap().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( + match seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), - min_efficiency, backoff, max_backoff_steps, + min_efficiency, backoff, max_backoff_steps ) { - state = better_state; - history.backoff_steps.push(backoff_steps); - } else { - return Realization { - result: Err("Line search failed".to_string()), - history, - }; - } + Some((better_state, backoff_steps)) => { + state = better_state; + history.backoff_steps.push(backoff_steps); + }, + None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) + }; } - let result = if state.loss < tol { + let success = state.loss < tol; + let tangent = if success { // express the uniform basis in the standard basis const UNIFORM_DIM: usize = 4; let total_dim_unif = UNIFORM_DIM * assembly_dim; @@ -507,13 +500,11 @@ 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 }) + ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) } else { - Err("Failed to reach target accuracy".to_string()) + ConfigSubspace::zero(assembly_dim) }; - Realization { result, history } + (state.config, tangent, success, history) } // --- tests --- @@ -532,12 +523,12 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, -9.0, 5.0), - sphere(0.0, 0.0, 11.0, 3.0), + sphere(0.0, 0.0, 11.0, 3.0) ].into_iter().chain( (1..=6).map( |k| { @@ -583,7 +574,7 @@ pub mod examples { // 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 { + pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -596,7 +587,7 @@ pub mod examples { point(0.0, 0.0, 0.0), point(ang_hor.cos(), ang_hor.sin(), 0.0), point(x_vert, y_vert, -0.5), - point(x_vert, y_vert, 0.5), + point(x_vert, y_vert, 0.5) ] } ).collect::>().as_slice() @@ -639,15 +630,15 @@ mod tests { MatrixEntry { index: (0, 0), value: 14.0 }, MatrixEntry { index: (0, 2), value: 28.0 }, MatrixEntry { index: (1, 1), value: 42.0 }, - MatrixEntry { index: (1, 2), value: 49.0 }, + MatrixEntry { index: (1, 2), value: 49.0 } ]); let config = DMatrix::::from_row_slice(2, 3, &[ 1.0, 2.0, 3.0, - 4.0, 5.0, 6.0, + 4.0, 5.0, 6.0 ]); let expected_result = DMatrix::::from_row_slice(2, 3, &[ 14.0, 2.0, 28.0, - 4.0, 42.0, 49.0, + 4.0, 42.0, 49.0 ]); assert_eq!(frozen.freeze(&config), expected_result); } @@ -658,15 +649,15 @@ mod tests { MatrixEntry { index: (0, 0), value: 19.0 }, MatrixEntry { index: (0, 2), value: 39.0 }, MatrixEntry { index: (1, 1), value: 59.0 }, - MatrixEntry { index: (1, 2), value: 69.0 }, + MatrixEntry { index: (1, 2), value: 69.0 } ]); let attempt = DMatrix::::from_row_slice(2, 3, &[ 1.0, 2.0, 3.0, - 4.0, 5.0, 6.0, + 4.0, 5.0, 6.0 ]); let expected_result = DMatrix::::from_row_slice(2, 3, &[ 18.0, 0.0, 36.0, - 0.0, 54.0, 63.0, + 0.0, 54.0, 63.0 ]); assert_eq!(target.sub_proj(&attempt), expected_result); } @@ -684,7 +675,7 @@ mod tests { DMatrix::from_columns(&[ sphere(1.0, 0.0, 0.0, a), sphere(-0.5, a, 0.0, a), - sphere(-0.5, -a, 0.0, a), + sphere(-0.5, -a, 0.0, a) ]) }; let state = SearchState::from_config(&gram, config); @@ -698,7 +689,7 @@ mod tests { fn frozen_entry_test() { let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 0.95), + sphere(0.0, 0.0, 0.0, 0.95) ]); for j in 0..2 { for k in j..2 { @@ -707,10 +698,10 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let Realization { result, history } = realize_gram( + let (config, _, success, history) = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - let config = result.unwrap().config; + assert_eq!(success, true); for base_step in history.base_step.into_iter() { for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); @@ -725,7 +716,7 @@ mod tests { 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; + let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -742,7 +733,7 @@ mod tests { let mut problem = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.0, -2.0), sphere(0.0, 0.0, 1.0, 1.0), - sphere(0.0, 0.0, -1.0, 1.0), + sphere(0.0, 0.0, -1.0, 1.0) ]); for j in 0..3 { for k in j..3 { @@ -752,11 +743,11 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let Realization { result, history } = realize_gram( + let (config, tangent, success, history) = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(config, problem.guess); + assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -772,8 +763,8 @@ mod tests { DMatrix::::from_column_slice(UNIFORM_DIM, assembly_dim, &[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5, -0.5, - 0.0, 0.0, -0.5, 0.5, - ]), + 0.0, 0.0, -0.5, 0.5 + ]) ]; let tangent_motions_std = vec![ basis_matrix((0, 1), element_dim, assembly_dim), @@ -783,8 +774,8 @@ mod tests { DMatrix::::from_column_slice(element_dim, assembly_dim, &[ 0.0, 0.0, 0.0, 0.00, 0.0, 0.0, 0.0, -1.0, -0.25, -1.0, - 0.0, 0.0, -1.0, 0.25, 1.0, - ]), + 0.0, 0.0, -1.0, 0.25, 1.0 + ]) ]; // confirm that the dimension of the tangent space is no greater than @@ -824,8 +815,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); - let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); + assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -860,10 +851,10 @@ mod tests { DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]), DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), - DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]), + DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]) ] } - ).collect::>(), + ).collect::>() ]; let tangent_motions_std = tangent_motions_unif.iter().map( |motion| DMatrix::from_columns( @@ -896,7 +887,7 @@ mod tests { 0.0, 1.0, 0.0, 0.0, dis[1], 0.0, 0.0, 1.0, 0.0, dis[2], 2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(), - 0.0, 0.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 0.0, 0.0, 1.0 ]) } @@ -908,16 +899,16 @@ mod tests { const SCALED_TOL: f64 = 1.0e-12; let mut problem_orig = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.5, 1.0), - sphere(0.0, 0.0, -0.5, 1.0), + sphere(0.0, 0.0, -0.5, 1.0) ]); problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let Realization { result: result_orig, history: history_orig } = realize_gram( + let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); + assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); // find another pair of spheres that meet at 120°. we'll think of this @@ -926,19 +917,19 @@ mod tests { let a = 0.5 * FRAC_1_SQRT_2; DMatrix::from_columns(&[ sphere(a, 0.0, 7.0 + a, 1.0), - sphere(-a, 0.0, 7.0 - a, 1.0), + sphere(-a, 0.0, 7.0 - a, 1.0) ]) }; let problem_tfm = ConstraintProblem { gram: problem_orig.gram, - frozen: problem_orig.frozen, guess: guess_tfm, + frozen: problem_orig.frozen }; - let Realization { result: result_tfm, history: history_tfm } = realize_gram( + let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); + assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); // project a nudge to the tangent space of the solution variety at the @@ -960,7 +951,7 @@ mod tests { 0.0, 1.0, 0.0, 0.0, 0.0, FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 0.0, 0.0, 1.0 ]); let transl = translation(Vector3::new(0.0, 0.0, 7.0)); let motion_proj_tfm = transl * rot * motion_orig_proj; diff --git a/app-proto/src/components/identity.vert b/app-proto/src/identity.vert similarity index 100% rename from app-proto/src/components/identity.vert rename to app-proto/src/identity.vert diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index a03b026..b76859a 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,6 +1,8 @@ +mod add_remove; mod assembly; -mod components; +mod display; mod engine; +mod outline; mod specified; #[cfg(test)] @@ -9,25 +11,22 @@ mod tests; use std::{collections::BTreeSet, rc::Rc}; use sycamore::prelude::*; +use add_remove::AddRemove; use assembly::{Assembly, Element}; -use components::{ - add_remove::AddRemove, - diagnostics::Diagnostics, - display::Display, - outline::Outline, -}; +use display::Display; +use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal>>, + selection: Signal>> } impl AppState { - fn new() -> Self { - Self { + fn new() -> AppState { + AppState { assembly: Assembly::new(), - selection: create_signal(BTreeSet::default()), + selection: create_signal(BTreeSet::default()) } } @@ -58,10 +57,9 @@ fn main() { provide_context(AppState::new()); view! { - div(id = "sidebar") { + div(id="sidebar") { AddRemove {} Outline {} - Diagnostics {} } Display {} } diff --git a/app-proto/src/components/outline.rs b/app-proto/src/outline.rs similarity index 75% rename from app-proto/src/components/outline.rs rename to app-proto/src/outline.rs index 5355042..caf11e8 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/outline.rs @@ -1,7 +1,11 @@ use itertools::Itertools; use std::rc::Rc; use sycamore::prelude::*; -use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; +use web_sys::{ + KeyboardEvent, + MouseEvent, + wasm_bindgen::JsCast +}; use crate::{ AppState, @@ -9,7 +13,7 @@ use crate::{ Element, HalfCurvatureRegulator, InversiveDistanceRegulator, - Regulator, + Regulator }, specified::SpecifiedValue }; @@ -45,8 +49,8 @@ fn RegulatorInput(regulator: Rc) -> View { view! { input( - r#type = "text", - class = move || { + r#type="text", + class=move || { if valid.get() { set_point.with(|set_pt| { if set_pt.is_present() { @@ -59,27 +63,27 @@ fn RegulatorInput(regulator: Rc) -> View { "regulator-input invalid" } }, - placeholder = measurement.with(|result| result.to_string()), - bind:value = value, - on:change = move |_| { + placeholder=measurement.with(|result| result.to_string()), + bind:value=value, + on:change=move |_| { valid.set( match SpecifiedValue::try_from(value.get_clone_untracked()) { Ok(set_pt) => { set_point.set(set_pt); true - }, - Err(_) => false, + } + Err(_) => false } ) }, - on:keydown = { + on:keydown={ move |event: KeyboardEvent| { match event.key().as_str() { "Escape" => reset_value(), - _ => (), + _ => () } } - }, + } ) } } @@ -96,11 +100,11 @@ impl OutlineItem for InversiveDistanceRegulator { self.subjects[0].label() }.clone(); view! { - li(class = "regulator") { - div(class = "regulator-label") { (other_subject_label) } - div(class = "regulator-type") { "Inversive distance" } - RegulatorInput(regulator = self) - div(class = "status") + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=self) + div(class="status") } } } @@ -109,11 +113,11 @@ impl OutlineItem for InversiveDistanceRegulator { impl OutlineItem for HalfCurvatureRegulator { fn outline_item(self: Rc, _element: &Rc) -> View { view! { - li(class = "regulator") { - div(class = "regulator-label") // for spacing - div(class = "regulator-type") { "Half-curvature" } - RegulatorInput(regulator = self) - div(class = "status") + li(class="regulator") { + div(class="regulator-label") // for spacing + div(class="regulator-type") { "Half-curvature" } + RegulatorInput(regulator=self) + div(class="status") } } } @@ -147,15 +151,15 @@ fn ElementOutlineItem(element: Rc) -> View { .clone() .into_iter() .sorted_by_key(|reg| reg.subjects().len()) - .collect::>() + .collect() ); let details_node = create_node_ref(); view! { li { - details(ref = details_node) { + details(ref=details_node) { summary( - class = class.get(), - on:keydown = { + class=class.get(), + on:keydown={ let element_for_handler = element.clone(); move |event: KeyboardEvent| { match event.key().as_str() { @@ -175,18 +179,18 @@ fn ElementOutlineItem(element: Rc) -> View { .unchecked_into::() .remove_attribute("open"); }, - _ => (), + _ => () } } } ) { div( - class = "element-switch", - on:click = |event: MouseEvent| event.stop_propagation() + class="element-switch", + on:click=|event: MouseEvent| event.stop_propagation() ) div( - class = "element", - on:click = { + class="element", + on:click={ let state_for_handler = state.clone(); let element_for_handler = element.clone(); move |event: MouseEvent| { @@ -196,20 +200,16 @@ fn ElementOutlineItem(element: Rc) -> View { } } ) { - div(class = "element-label") { (label) } - div(class = "element-representation") { (rep_components) } - input( - r#type = "checkbox", - bind:checked = element.ghost(), - on:click = |event: MouseEvent| event.stop_propagation() - ) + div(class="element-label") { (label) } + div(class="element-representation") { (rep_components) } + div(class="status") } } - ul(class = "regulators") { + ul(class="regulators") { Keyed( - list = regulator_list, - view = move |reg| reg.outline_item(&element), - key = |reg| reg.serial() + list=regulator_list, + view=move |reg| reg.outline_item(&element), + key=|reg| reg.serial() ) } } @@ -237,23 +237,23 @@ pub fn Outline() -> View { .clone() .into_iter() .sorted_by_key(|elt| elt.id().clone()) - .collect::>() + .collect() ); view! { ul( - id = "outline", - on:click = { + id="outline", + on:click={ let state = use_context::(); move |_| state.selection.update(|sel| sel.clear()) } ) { Keyed( - list = element_list, - view = |elt| view! { - ElementOutlineItem(element = elt) + list=element_list, + view=|elt| view! { + ElementOutlineItem(element=elt) }, - key = |elt| elt.serial() + key=|elt| elt.serial() ) } } diff --git a/app-proto/src/components/point.frag b/app-proto/src/point.frag similarity index 60% rename from app-proto/src/components/point.frag rename to app-proto/src/point.frag index 194a072..3a361a8 100644 --- a/app-proto/src/components/point.frag +++ b/app-proto/src/point.frag @@ -2,7 +2,7 @@ precision highp float; -in vec4 point_color; +in vec3 point_color; in float point_highlight; in float total_radius; @@ -13,7 +13,6 @@ void main() { 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; + vec3 color = mix(point_color, vec3(1.), border * point_highlight); + outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); } \ No newline at end of file diff --git a/app-proto/src/components/point.vert b/app-proto/src/point.vert similarity index 91% rename from app-proto/src/components/point.vert rename to app-proto/src/point.vert index 0b76bc1..6945010 100644 --- a/app-proto/src/components/point.vert +++ b/app-proto/src/point.vert @@ -1,11 +1,11 @@ #version 300 es in vec4 position; -in vec4 color; +in vec3 color; in float highlight; in float selected; -out vec4 point_color; +out vec3 point_color; out float point_highlight; out float total_radius; diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index 788460b..cfe7fc3 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -13,12 +13,12 @@ use std::num::ParseFloatError; #[readonly::make] pub struct SpecifiedValue { pub spec: String, - pub value: Option, + pub value: Option } impl SpecifiedValue { - pub fn from_empty_spec() -> Self { - Self { spec: String::new(), value: None } + pub fn from_empty_spec() -> SpecifiedValue { + SpecifiedValue { 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(Self::from_empty_spec()) + Ok(SpecifiedValue::from_empty_spec()) } else { spec.parse::().map( - |value| Self { spec, value: Some(value) } + |value| SpecifiedValue { spec: spec, value: Some(value) } ) } } diff --git a/app-proto/src/components/spheres.frag b/app-proto/src/spheres.frag similarity index 95% rename from app-proto/src/components/spheres.frag rename to app-proto/src/spheres.frag index fa317a8..d50cb1e 100644 --- a/app-proto/src/components/spheres.frag +++ b/app-proto/src/spheres.frag @@ -17,7 +17,7 @@ struct vecInv { const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec4 color_list[SPHERE_MAX]; +uniform vec3 color_list[SPHERE_MAX]; uniform float highlight_list[SPHERE_MAX]; // view @@ -25,6 +25,7 @@ uniform vec2 resolution; uniform float shortdim; // controls +uniform float opacity; uniform int layer_threshold; uniform bool debug_mode; @@ -68,7 +69,7 @@ struct Fragment { vec4 color; }; -Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) { +Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector // and the sphere vector with respect to the coordinates of the impact @@ -78,7 +79,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) { 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)); + return Fragment(pt, normal, vec4(illum * base_color, opacity)); } float intersection_dist(Fragment a, Fragment b) { @@ -191,11 +192,10 @@ void main() { vec3 color = vec3(0.); int layer = layer_cnt - 1; TaggedDepth hit = top_hits[layer]; - vec4 sphere_color = color_list[hit.id]; Fragment frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - vec4(hit.dimming * sphere_color.rgb, sphere_color.a) + hit.dimming * color_list[hit.id] ); float highlight_next = highlight_list[hit.id]; --layer; @@ -206,11 +206,10 @@ void main() { // shade the next fragment hit = top_hits[layer]; - sphere_color = color_list[hit.id]; frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - vec4(hit.dimming * sphere_color.rgb, sphere_color.a) + hit.dimming * color_list[hit.id] ); highlight_next = highlight_list[hit.id]; diff --git a/deploy/.gitignore b/deploy/.gitignore deleted file mode 100644 index 192f529..0000000 --- a/deploy/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/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 deleted file mode 100644 index fdda434..0000000 --- a/tools/package-for-deployment.sh +++ /dev/null @@ -1,16 +0,0 @@ -# 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/tools/run-examples.sh b/tools/run-examples.sh deleted file mode 100644 index 0946d92..0000000 --- a/tools/run-examples.sh +++ /dev/null @@ -1,20 +0,0 @@ -# run all Cargo examples, as described here: -# -# Karol Kuczmarski. "Add examples to your Rust libraries" -# http://xion.io/post/code/rust-examples.html -# -# you should invoke this script by calling `sh` or another interpreter, rather -# than calling `souce`, to ensure that the script can find the manifest file for -# the application prototype - -# find the manifest file for the application prototype -MANIFEST="$(dirname -- $0)/../app-proto/Cargo.toml" - -# set up the command that runs each example -RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" - -# run the examples -$RUN_EXAMPLE irisawa-hexlet; echo -$RUN_EXAMPLE three-spheres; echo -$RUN_EXAMPLE point-on-sphere; echo -$RUN_EXAMPLE kaleidocycle \ No newline at end of file