Compare commits

..

1 commit

Author SHA1 Message Date
Aaron Fenyes
a4d081f684 Update Sycamore to 0.9.1
All checks were successful
/ test (pull_request) Successful in 2m30s
2025-06-09 22:18:37 -07:00
30 changed files with 723 additions and 2461 deletions

View file

@ -25,37 +25,32 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
### Install the prerequisites ### Install the prerequisites
1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager 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" 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) 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/) 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 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 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 * 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 * On POSIX systems, the search path is stored in the `PATH` environment variable
### Play with the prototype ### Play with the prototype
1. From the `app-proto` folder, call `trunk serve --release` to build and serve 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 * *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 * *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. * *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:` 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 4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
### Run the engine on some example problems ### Run the engine on some example problems
1. Use `sh` to run the script `tools/run-examples.sh` 1. Go into the `app-proto` folder
- The script is location-independent, so you can do this from anywhere in the dyna3 repository 2. Call `./run-examples`
- The call from the top level of the repository is: * *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*
```bash
sh tools/run-examples.sh
```
- For each example problem, the engine will print the value of the loss function at each optimization step
- The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then
```julia ```julia
include("irisawa-hexlet.jl") include("irisawa-hexlet.jl")
@ -64,24 +59,9 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
end 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 ### Run the automated tests
1. Go into the `app-proto` folder 1. Go into the `app-proto` folder
2. Call `cargo test` 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

565
app-proto/Cargo.lock generated
View file

@ -20,21 +20,6 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 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]] [[package]]
name = "approx" name = "approx"
version = "0.5.1" version = "0.5.1"
@ -50,21 +35,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@ -98,35 +68,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -137,122 +78,10 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "dyna3" name = "dyna3"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"charming",
"console_error_panic_hook", "console_error_panic_hook",
"dyna3", "dyna3",
"itertools", "itertools",
@ -277,22 +106,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@ -304,28 +117,6 @@ dependencies = [
"wasi", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.14.5"
@ -336,12 +127,6 @@ dependencies = [
"allocator-api2", "allocator-api2",
] ]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "html-escape" name = "html-escape"
version = "0.2.13" version = "0.2.13"
@ -351,47 +136,6 @@ dependencies = [
"utf8-width", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.5.0" version = "2.5.0"
@ -399,8 +143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.5", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -412,12 +155,6 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.70" version = "0.3.70"
@ -455,12 +192,6 @@ dependencies = [
"rawpointer", "rawpointer",
] ]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "minicov" name = "minicov"
version = "0.3.5" version = "0.3.5"
@ -517,12 +248,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.46"
@ -532,21 +257,6 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-rational" name = "num-rational"
version = "0.4.2" version = "0.4.2"
@ -579,57 +289,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 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]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.20"
@ -704,12 +363,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "safe_arch" name = "safe_arch"
version = "0.7.2" version = "0.7.2"
@ -734,90 +387,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -852,20 +421,14 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "sycamore" name = "sycamore"
version = "0.9.1" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc"
dependencies = [ dependencies = [
"hashbrown 0.14.5", "hashbrown",
"indexmap 2.5.0", "indexmap",
"paste", "paste",
"sycamore-core", "sycamore-core",
"sycamore-macro", "sycamore-macro",
@ -881,7 +444,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187"
dependencies = [ dependencies = [
"hashbrown 0.14.5", "hashbrown",
"paste", "paste",
"sycamore-reactive", "sycamore-reactive",
] ]
@ -943,78 +506,21 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.87" version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "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]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"
@ -1171,65 +677,6 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"

View file

@ -17,9 +17,6 @@ nalgebra = "0.33.0"
readonly = "0.2.12" readonly = "0.2.12"
sycamore = "0.9.1" sycamore = "0.9.1"
# We use Charming to help display engine diagnostics
charming = { version = "0.5.1", features = ["wasm"] }
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires # logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for

View file

@ -1,2 +0,0 @@
[build]
public_url = "./"

View file

@ -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<f64>) {
println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end());
}
pub fn config(config: &DMatrix<f64>) {
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);
}
}

View file

@ -1,23 +1,25 @@
#[path = "common/print.rs"] use dyna3::engine::{Q, examples::realize_irisawa_hexlet};
mod print;
use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet};
fn main() { fn main() {
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let realization = realize_irisawa_hexlet(SCALED_TOL); let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL);
print::title("Irisawa hexlet"); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
print::realization_diagnostics(&realization); if success {
if let Ok(ConfigNeighborhood { config, .. }) = realization.result { println!("Target accuracy achieved!");
// print the diameters of the chain spheres } 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!("\nChain diameters:");
println!(" {} sun (given)", 1.0 / config[(3, 3)]); println!(" {} sun (given)", 1.0 / config[(3, 3)]);
for k in 4..9 { for k in 4..9 {
println!(" {} sun", 1.0 / config[(3, k)]); 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);
}
} }

View file

@ -1,32 +1,30 @@
#[path = "common/print.rs"]
mod print;
use nalgebra::{DMatrix, DVector}; use nalgebra::{DMatrix, DVector};
use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; use dyna3::engine::{Q, examples::realize_kaleidocycle};
fn main() { fn main() {
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let realization = realize_kaleidocycle(SCALED_TOL); let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
print::title("Kaleidocycle"); print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config);
print::realization_diagnostics(&realization); print!("Configuration:{}", config);
if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { if success {
// print the completed Gram matrix and the realized configuration println!("Target accuracy achieved!");
print::gram_matrix(&config); } else {
print::config(&config); 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 // find the kaleidocycle's twist motion by projecting onto the tangent space
// space
const N_POINTS: usize = 12; const N_POINTS: usize = 12;
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
let down = -&up; let down = -&up;
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|n| [ |n| [
tangent.proj(&up.as_view(), n), tangent.proj(&up.as_view(), n),
tangent.proj(&down.as_view(), n+1), tangent.proj(&down.as_view(), n+1)
] ]
).sum(); ).sum();
let normalization = 5.0 / twist_motion[(2, 0)]; let normalization = 5.0 / twist_motion[(2, 0)];
println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); print!("Twist motion:{}", normalization * twist_motion);
}
} }

View file

@ -1,13 +1,4 @@
#[path = "common/print.rs"] use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem};
mod print;
use dyna3::engine::{
point,
realize_gram,
sphere,
ConfigNeighborhood,
ConstraintProblem,
};
fn main() { fn main() {
let mut problem = ConstraintProblem::from_guess(&[ let mut problem = ConstraintProblem::from_guess(&[
@ -20,14 +11,21 @@ fn main() {
} }
} }
problem.frozen.push(3, 0, problem.guess[(3, 0)]); 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 &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
print::title("Point on a sphere"); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
print::realization_diagnostics(&realization); print!("Configuration:{}", config);
if let Ok(ConfigNeighborhood { config, .. }) = realization.result { if success {
print::gram_matrix(&config); println!("Target accuracy achieved!");
print::config(&config); } 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);
} }

View file

@ -1,12 +1,4 @@
#[path = "common/print.rs"] use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem};
mod print;
use dyna3::engine::{
realize_gram,
sphere,
ConfigNeighborhood,
ConstraintProblem,
};
fn main() { fn main() {
let mut problem = ConstraintProblem::from_guess({ let mut problem = ConstraintProblem::from_guess({
@ -14,7 +6,7 @@ fn main() {
&[ &[
sphere(1.0, 0.0, 0.0, 1.0), 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), sphere(-0.5, -a, 0.0, 1.0)
] ]
}); });
for j in 0..3 { 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 }); 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 &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
print::title("Three spheres"); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
print::realization_diagnostics(&realization); if success {
if let Ok(ConfigNeighborhood { config, .. }) = realization.result { println!("Target accuracy achieved!");
print::gram_matrix(&config); } 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);
} }

View file

@ -6,12 +6,6 @@
<link data-trunk rel="css" href="main.css"/> <link data-trunk rel="css" href="main.css"/>
<link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet"> <link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
<link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet"> <link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet">
<!--
the Charming visualization crate, which we use to show engine diagnostics,
depends the ECharts JavaScript package
-->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
</head> </head>
<body></body> <body></body>
</html> </html>

View file

@ -18,17 +18,6 @@ body {
font-family: 'Fira Sans', sans-serif; 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 */
#sidebar { #sidebar {
@ -149,7 +138,6 @@ details[open]:has(li) .element-switch::after {
} }
.regulator-input { .regulator-input {
margin-right: 4px;
color: inherit; color: inherit;
background-color: inherit; background-color: inherit;
border: 1px solid var(--border); border: 1px solid var(--border);
@ -171,56 +159,22 @@ details[open]:has(li) .element-switch::after {
border-color: var(--border-invalid); 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 { .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
content: '⚠'; content: '⚠';
color: var(--text-invalid); 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 */
#display { canvas {
float: left; float: left;
margin-left: 20px; margin-left: 20px;
margin-top: 20px; margin-top: 20px;
@ -229,7 +183,7 @@ details[open]:has(li) .element-switch::after {
border-radius: 16px; border-radius: 16px;
} }
#display:focus { canvas:focus {
border-color: var(--border-focus-dark); border-color: var(--border-focus-dark);
outline: none; outline: none;
} }

12
app-proto/run-examples Executable file
View file

@ -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

257
app-proto/src/add_remove.rs Normal file
View file

@ -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::<AppState>();
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::<AppState>();
state.assembly.insert_element_default::<Sphere>();
}
) { "Add sphere" }
button(
on:click=|_| {
let state = use_context::<AppState>();
state.assembly.insert_element_default::<Point>();
}
) { "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::<AppState>();
state.selection.with(|sel| sel.len() != 2)
},
on:click=|_| {
let state = use_context::<AppState>();
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::<Vec<_>>()
.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" }
}
}
}
}

View file

@ -1,34 +1,33 @@
use nalgebra::{DMatrix, DVector, DVectorView}; use nalgebra::{DMatrix, DVector, DVectorView};
use std::{ use std::{
cell::Cell, cell::Cell,
cmp::Ordering,
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
cmp::Ordering,
fmt, fmt,
fmt::{Debug, Formatter}, fmt::{Debug, Formatter},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
rc::Rc, rc::Rc,
sync::{atomic, atomic::AtomicU64}, sync::{atomic, atomic::AtomicU64}
}; };
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::{ use crate::{
components::{display::DisplayItem, outline::OutlineItem}, display::DisplayItem,
engine::{ engine::{
Q, Q,
change_half_curvature,
local_unif_to_std, local_unif_to_std,
point, point,
project_point_to_normalized, project_point_to_normalized,
project_sphere_to_normalized, project_sphere_to_normalized,
realize_gram, realize_gram,
sphere, sphere,
ConfigNeighborhood,
ConfigSubspace, ConfigSubspace,
ConstraintProblem, ConstraintProblem
DescentHistory,
Realization,
}, },
specified::SpecifiedValue, outline::OutlineItem,
specified::SpecifiedValue
}; };
pub type ElementColor = [f32; 3]; pub type ElementColor = [f32; 3];
@ -164,7 +163,7 @@ pub struct Sphere {
pub ghost: Signal<bool>, pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>>, column_index: Cell<Option<usize>>
} }
impl Sphere { impl Sphere {
@ -174,17 +173,17 @@ impl Sphere {
id: String, id: String,
label: String, label: String,
color: ElementColor, color: ElementColor,
representation: DVector<f64>, representation: DVector<f64>
) -> Self { ) -> Sphere {
Self { Sphere {
id, id: id,
label, label: label,
color, color: color,
representation: create_signal(representation), representation: create_signal(representation),
ghost: create_signal(false), ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into(), column_index: None.into()
} }
} }
} }
@ -194,12 +193,12 @@ impl Element for Sphere {
"sphere".to_string() "sphere".to_string()
} }
fn default(id: String, id_num: u64) -> Self { fn default(id: String, id_num: u64) -> Sphere {
Self::new( Sphere::new(
id, id,
format!("Sphere {id_num}"), format!("Sphere {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32], [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)
) )
} }
@ -264,7 +263,7 @@ pub struct Point {
pub ghost: Signal<bool>, pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>>, column_index: Cell<Option<usize>>
} }
impl Point { impl Point {
@ -274,9 +273,9 @@ impl Point {
id: String, id: String,
label: String, label: String,
color: ElementColor, color: ElementColor,
representation: DVector<f64>, representation: DVector<f64>
) -> Self { ) -> Point {
Self { Point {
id, id,
label, label,
color, color,
@ -284,7 +283,7 @@ impl Point {
ghost: create_signal(false), ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into(), column_index: None.into()
} }
} }
} }
@ -294,12 +293,12 @@ impl Element for Point {
"point".to_string() "point".to_string()
} }
fn default(id: String, id_num: u64) -> Self { fn default(id: String, id_num: u64) -> Point {
Self::new( Point::new(
id, id,
format!("Point {id_num}"), format!("Point {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
point(0.0, 0.0, 0.0), point(0.0, 0.0, 0.0)
) )
} }
@ -348,7 +347,7 @@ impl ProblemPoser for Point {
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
); );
problem.gram.push_sym(index, index, 0.0); 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()); problem.guess.set_column(index, &self.representation.get_clone_untracked());
} }
} }
@ -357,6 +356,16 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<Rc<dyn Element>>; fn subjects(&self) -> Vec<Rc<dyn Element>>;
fn measurement(&self) -> ReadSignal<f64>; fn measurement(&self) -> ReadSignal<f64>;
fn set_point(&self) -> Signal<SpecifiedValue>; fn set_point(&self) -> Signal<SpecifiedValue>;
// 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 { impl Hash for dyn Regulator {
@ -389,11 +398,11 @@ pub struct InversiveDistanceRegulator {
pub subjects: [Rc<dyn Element>; 2], pub subjects: [Rc<dyn Element>; 2],
pub measurement: ReadSignal<f64>, pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>, pub set_point: Signal<SpecifiedValue>,
serial: u64, serial: u64
} }
impl InversiveDistanceRegulator { impl InversiveDistanceRegulator {
pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self { pub fn new(subjects: [Rc<dyn Element>; 2]) -> InversiveDistanceRegulator {
let representations = subjects.each_ref().map(|subj| subj.representation()); let representations = subjects.each_ref().map(|subj| subj.representation());
let measurement = create_memo(move || { let measurement = create_memo(move || {
representations[0].with(|rep_0| representations[0].with(|rep_0|
@ -406,7 +415,7 @@ impl InversiveDistanceRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec()); let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial(); let serial = Self::next_serial();
Self { subjects, measurement, set_point, serial } InversiveDistanceRegulator { subjects, measurement, set_point, serial }
} }
} }
@ -449,11 +458,11 @@ pub struct HalfCurvatureRegulator {
pub subject: Rc<dyn Element>, pub subject: Rc<dyn Element>,
pub measurement: ReadSignal<f64>, pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>, pub set_point: Signal<SpecifiedValue>,
serial: u64, serial: u64
} }
impl HalfCurvatureRegulator { impl HalfCurvatureRegulator {
pub fn new(subject: Rc<dyn Element>) -> Self { pub fn new(subject: Rc<dyn Element>) -> HalfCurvatureRegulator {
let measurement = subject.representation().map( let measurement = subject.representation().map(
|rep| rep[Sphere::CURVATURE_COMPONENT] |rep| rep[Sphere::CURVATURE_COMPONENT]
); );
@ -461,7 +470,7 @@ impl HalfCurvatureRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec()); let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial(); let serial = Self::next_serial();
Self { subject, measurement, set_point, serial } HalfCurvatureRegulator { subject, measurement, set_point, serial }
} }
} }
@ -477,6 +486,18 @@ impl Regulator for HalfCurvatureRegulator {
fn set_point(&self) -> Signal<SpecifiedValue> { fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point 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 { impl Serial for HalfCurvatureRegulator {
@ -501,7 +522,7 @@ impl ProblemPoser for HalfCurvatureRegulator {
// the velocity is expressed in uniform coordinates // the velocity is expressed in uniform coordinates
pub struct ElementMotion<'a> { pub struct ElementMotion<'a> {
pub element: Rc<dyn Element>, pub element: Rc<dyn Element>,
pub velocity: DVectorView<'a, f64>, pub velocity: DVectorView<'a, f64>
} }
type AssemblyMotion<'a> = Vec<ElementMotion<'a>>; type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
@ -526,44 +547,17 @@ pub struct Assembly {
pub tangent: Signal<ConfigSubspace>, pub tangent: Signal<ConfigSubspace>,
// indexing // indexing
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>, pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>
// realization control
pub realization_trigger: Signal<()>,
// realization diagnostics
pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>,
} }
impl Assembly { impl Assembly {
pub fn new() -> Assembly { pub fn new() -> Assembly {
// create an assembly Assembly {
let assembly = Assembly {
elements: create_signal(BTreeSet::new()), elements: create_signal(BTreeSet::new()),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
tangent: create_signal(ConfigSubspace::zero(0)), tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(BTreeMap::default()), 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
} }
// --- inserting elements and regulators --- // --- inserting elements and regulators ---
@ -624,6 +618,19 @@ impl Assembly {
regulators.update(|regs| regs.insert(regulator.clone())); 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!("Updated regulator with subjects {:?}", regulator.subjects());
if regulator.try_activate() {
self_for_effect.realize();
}
});
/* DEBUG */ /* DEBUG */
// print an updated list of regulators // print an updated list of regulators
console_log!("Regulators:"); console_log!("Regulators:");
@ -680,34 +687,22 @@ impl Assembly {
console_log!("Old configuration:{:>8.3}", problem.guess); console_log!("Old configuration:{:>8.3}", problem.guess);
// look for a configuration with the given Gram matrix // 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 &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
/* DEBUG */ /* DEBUG */
// report the outcome of the search in the browser console // report the outcome of the search
if let Err(ref message) = result { if success {
console_log!("❌️ {message}"); console_log!("Target accuracy achieved!")
} else { } else {
console_log!("✅️ Target accuracy achieved!"); console_log!("Failed to reach target accuracy")
} }
if history.scaled_loss.len() > 0 {
console_log!("Steps: {}", history.scaled_loss.len() - 1); console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Loss: {}", history.scaled_loss.last().unwrap()); console_log!("Loss: {}", *history.scaled_loss.last().unwrap());
}
// 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()); console_log!("Tangent dimension: {}", tangent.dim());
// report the realization status if success {
self.realization_status.set(Ok(()));
// read out the solution // read out the solution
for elt in self.elements.get_clone_untracked() { for elt in self.elements.get_clone_untracked() {
elt.representation().update( elt.representation().update(
@ -717,14 +712,6 @@ impl Assembly {
// save the tangent space // save the tangent space
self.tangent.set_silent(tangent); 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))
},
} }
} }
@ -807,15 +794,15 @@ impl Assembly {
}, },
None => { None => {
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
}, }
}; };
}); });
} }
// trigger a realization to bring the configuration back onto the // bring the configuration back onto the solution variety. this also
// solution variety. this also gets the elements' column indices and the // gets the elements' column indices and the saved tangent space back in
// saved tangent space back in sync // sync
self.realization_trigger.set(()); self.realize();
} }
} }
@ -867,7 +854,7 @@ mod tests {
String::from(sphere_id), String::from(sphere_id),
String::from("Sphere 0"), String::from("Sphere 0"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS), engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS)
) )
); );
@ -881,7 +868,7 @@ mod tests {
vec![ vec![
ElementMotion { ElementMotion {
element: sphere.clone(), element: sphere.clone(),
velocity: velocity.as_view(), velocity: velocity.as_view()
} }
] ]
); );

View file

@ -1,5 +0,0 @@
pub mod add_remove;
pub mod diagnostics;
pub mod display;
pub mod outline;
pub mod test_assembly_chooser;

View file

@ -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::<AppState>();
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::<Sphere>();
});
}
) { "Add sphere" }
button(
on:click = |_| {
let state = use_context::<AppState>();
state.assembly.insert_element_default::<Point>();
}
) { "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::<AppState>();
state.selection.with(|sel| sel.len() != 2)
},
on:click = |_| {
let state = use_context::<AppState>();
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::<Vec<_>>()
.try_into()
.unwrap()
);
state.assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new(subjects))
);
state.selection.update(|sel| sel.clear());
}
) { "🔗" }
TestAssemblyChooser {}
}
}
}

View file

@ -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<String>,
}
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::<AppState>();
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<Option<f64>> {
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::<AppState>();
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::<f64>]]
}
);
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::<AppState>();
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::<f64>]]
}
);
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::<f64>]]
}
);
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::<f64>]]
}
);
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::<DiagnosticsState>();
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 {} }
}
}
}

View file

@ -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::<Rc<dyn Element>>::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::<AppState>();
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" }
}
}
}

View file

@ -12,12 +12,12 @@ use web_sys::{
WebGlProgram, WebGlProgram,
WebGlShader, WebGlShader,
WebGlUniformLocation, WebGlUniformLocation,
wasm_bindgen::{JsCast, JsValue}, wasm_bindgen::{JsCast, JsValue}
}; };
use crate::{ use crate::{
AppState, AppState,
assembly::{Element, ElementColor, ElementMotion, Point, Sphere}, assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
}; };
// --- color --- // --- color ---
@ -37,15 +37,15 @@ fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
struct SceneSpheres { struct SceneSpheres {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>, highlights: Vec<f32>
} }
impl SceneSpheres { impl SceneSpheres {
fn new() -> Self { fn new() -> SceneSpheres{
Self { SceneSpheres {
representations: Vec::new(), representations: Vec::new(),
colors_with_opacity: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new(), highlights: Vec::new()
} }
} }
@ -53,10 +53,7 @@ impl SceneSpheres {
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
} }
fn push( fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32) {
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32,
) {
self.representations.push(representation); self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity)); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
@ -67,23 +64,20 @@ struct ScenePoints {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>, highlights: Vec<f32>,
selections: Vec<f32>, selections: Vec<f32>
} }
impl ScenePoints { impl ScenePoints {
fn new() -> Self { fn new() -> ScenePoints {
Self { ScenePoints {
representations: Vec::new(), representations: Vec::new(),
colors_with_opacity: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new(), highlights: Vec::new(),
selections: Vec::new(), selections: Vec::new()
} }
} }
fn push( fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) {
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
) {
self.representations.push(representation); self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity)); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
@ -93,14 +87,14 @@ impl ScenePoints {
pub struct Scene { pub struct Scene {
spheres: SceneSpheres, spheres: SceneSpheres,
points: ScenePoints, points: ScenePoints
} }
impl Scene { impl Scene {
fn new() -> Self { fn new() -> Scene {
Self { Scene {
spheres: SceneSpheres::new(), spheres: SceneSpheres::new(),
points: ScenePoints::new(), points: ScenePoints::new()
} }
} }
} }
@ -111,12 +105,7 @@ pub trait DisplayItem {
// the smallest positive depth, represented as a multiple of `dir`, where // the smallest positive depth, represented as a multiple of `dir`, where
// the line generated by `dir` hits the element. returns `None` if the line // the line generated by `dir` hits the element. returns `None` if the line
// misses the element // misses the element
fn cast( fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64>;
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
pixel_size: f64,
) -> Option<f64>;
} }
impl DisplayItem for Sphere { impl DisplayItem for Sphere {
@ -135,12 +124,7 @@ impl DisplayItem for Sphere {
// this method should be kept synchronized with `sphere_cast` in // this method should be kept synchronized with `sphere_cast` in
// `spheres.frag`, which does essentially the same thing on the GPU side // `spheres.frag`, which does essentially the same thing on the GPU side
fn cast( fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, _pixel_size: f64) -> Option<f64> {
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
_pixel_size: f64,
) -> Option<f64> {
// if `a/b` is less than this threshold, we approximate // if `a/b` is less than this threshold, we approximate
// `a*u^2 + b*u + c` by the linear function `b*u + c` // `a*u^2 + b*u + c` by the linear function `b*u + c`
const DEG_THRESHOLD: f64 = 1e-9; const DEG_THRESHOLD: f64 = 1e-9;
@ -193,12 +177,7 @@ impl DisplayItem for Point {
} }
/* SCAFFOLDING */ /* SCAFFOLDING */
fn cast( fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64> {
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
pixel_size: f64,
) -> Option<f64> {
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
if rep[2] < 0.0 { if rep[2] < 0.0 {
// this constant should be kept synchronized with `point.frag` // this constant should be kept synchronized with `point.frag`
@ -241,7 +220,7 @@ fn compile_shader(
fn set_up_program( fn set_up_program(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
vertex_shader_source: &str, vertex_shader_source: &str,
fragment_shader_source: &str, fragment_shader_source: &str
) -> WebGlProgram { ) -> WebGlProgram {
// compile the shaders // compile the shaders
let vertex_shader = compile_shader( let vertex_shader = compile_shader(
@ -281,12 +260,12 @@ fn get_uniform_array_locations<const N: usize>(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
program: &WebGlProgram, program: &WebGlProgram,
var_name: &str, var_name: &str,
member_name_opt: Option<&str>, member_name_opt: Option<&str>
) -> [Option<WebGlUniformLocation>; N] { ) -> [Option<WebGlUniformLocation>; N] {
array::from_fn(|n| { array::from_fn(|n| {
let name = match member_name_opt { let name = match member_name_opt {
Some(member_name) => format!("{var_name}[{n}].{member_name}"), 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()) context.get_uniform_location(&program, name.as_str())
}) })
@ -297,7 +276,7 @@ fn bind_to_attribute(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
attr_index: u32, attr_index: u32,
attr_size: i32, attr_size: i32,
buffer: &Option<WebGlBuffer>, buffer: &Option<WebGlBuffer>
) { ) {
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
context.vertex_attrib_pointer_with_i32( context.vertex_attrib_pointer_with_i32(
@ -313,7 +292,7 @@ fn bind_to_attribute(
// load the given data into a new vertex buffer object // load the given data into a new vertex buffer object
fn load_new_buffer( fn load_new_buffer(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
data: &[f32], data: &[f32]
) -> Option<WebGlBuffer> { ) -> Option<WebGlBuffer> {
// create a buffer and bind it to ARRAY_BUFFER // create a buffer and bind it to ARRAY_BUFFER
let buffer = context.create_buffer(); let buffer = context.create_buffer();
@ -340,7 +319,7 @@ fn bind_new_buffer_to_attribute(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
attr_index: u32, attr_index: u32,
attr_size: i32, attr_size: i32,
data: &[f32], data: &[f32]
) { ) {
let buffer = load_new_buffer(context, data); let buffer = load_new_buffer(context, data);
bind_to_attribute(context, attr_index, attr_size, &buffer); bind_to_attribute(context, attr_index, attr_size, &buffer);
@ -362,9 +341,9 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
Vector3::new( Vector3::new(
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, 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, 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
) )
} }
@ -464,14 +443,14 @@ pub fn Display() -> View {
let sphere_program = set_up_program( let sphere_program = set_up_program(
&ctx, &ctx,
include_str!("identity.vert"), include_str!("identity.vert"),
include_str!("spheres.frag"), include_str!("spheres.frag")
); );
// set up the point rendering program // set up the point rendering program
let point_program = set_up_program( let point_program = set_up_program(
&ctx, &ctx,
include_str!("point.vert"), include_str!("point.vert"),
include_str!("point.frag"), include_str!("point.frag")
); );
/* DEBUG */ /* DEBUG */
@ -488,7 +467,7 @@ pub fn Display() -> View {
// capped at 1024 elements // capped at 1024 elements
console::log_2( console::log_2(
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), &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 // find the sphere program's vertex attribute
@ -524,7 +503,7 @@ pub fn Display() -> View {
// southeast triangle // 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,
1.0, -1.0, 0.0, 1.0, -1.0, 0.0
]; ];
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
@ -617,7 +596,7 @@ pub fn Display() -> View {
vec![ vec![
ElementMotion { ElementMotion {
element: sel, element: sel,
velocity: elt_motion.as_view(), velocity: elt_motion.as_view()
} }
] ]
); );
@ -650,7 +629,7 @@ pub fn Display() -> View {
0.0, 1.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, u, 0.0, 0.0, 1.0, 0.0, u,
0.0, 0.0, 2.0*u, 1.0, u*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; let asm_to_world = &location * &orientation;
@ -689,19 +668,19 @@ pub fn Display() -> View {
let v = &sphere_reps_world[n]; let v = &sphere_reps_world[n];
ctx.uniform3fv_with_f32_array( ctx.uniform3fv_with_f32_array(
sphere_sp_locs[n].as_ref(), sphere_sp_locs[n].as_ref(),
v.rows(0, 3).as_slice(), v.rows(0, 3).as_slice()
); );
ctx.uniform2fv_with_f32_array( ctx.uniform2fv_with_f32_array(
sphere_lt_locs[n].as_ref(), sphere_lt_locs[n].as_ref(),
v.rows(3, 2).as_slice(), v.rows(3, 2).as_slice()
); );
ctx.uniform4fv_with_f32_array( ctx.uniform4fv_with_f32_array(
sphere_color_locs[n].as_ref(), sphere_color_locs[n].as_ref(),
&scene.spheres.colors_with_opacity[n], &scene.spheres.colors_with_opacity[n]
); );
ctx.uniform1f( ctx.uniform1f(
sphere_highlight_locs[n].as_ref(), sphere_highlight_locs[n].as_ref(),
scene.spheres.highlights[n], scene.spheres.highlights[n]
); );
} }
@ -794,7 +773,7 @@ pub fn Display() -> View {
"ArrowLeft" if shift => roll_ccw.set(value), "ArrowLeft" if shift => roll_ccw.set(value),
"ArrowRight" => yaw_right.set(value), "ArrowRight" => yaw_right.set(value),
"ArrowLeft" => yaw_left.set(value), "ArrowLeft" => yaw_left.set(value),
_ => navigating = false, _ => navigating = false
}; };
if navigating { if navigating {
scene_changed.set(true); scene_changed.set(true);
@ -814,7 +793,7 @@ pub fn Display() -> View {
"s" | "S" => translate_neg_y.set(value), "s" | "S" => translate_neg_y.set(value),
"]" | "}" => shrink_neg.set(value), "]" | "}" => shrink_neg.set(value),
"[" | "{" => shrink_pos.set(value), "[" | "{" => shrink_pos.set(value),
_ => manipulating = false, _ => manipulating = false
}; };
if manipulating { if manipulating {
event.prevent_default(); event.prevent_default();
@ -827,7 +806,6 @@ pub fn Display() -> View {
// again // again
canvas( canvas(
ref=display, ref=display,
id = "display",
width="600", width="600",
height="600", height="600",
tabindex="0", tabindex="0",
@ -904,18 +882,18 @@ pub fn Display() -> View {
clicked = Some((elt, depth)) clicked = Some((elt, depth))
} }
}, },
None => clicked = Some((elt, depth)), None => clicked = Some((elt, depth))
}, }
None => (), None => ()
}; };
} }
// if we clicked something, select it // if we clicked something, select it
match clicked { match clicked {
Some((elt, _)) => state.select(&elt, event.shift_key()), Some((elt, _)) => state.select(&elt, event.shift_key()),
None => state.selection.update(|sel| sel.clear()), None => state.selection.update(|sel| sel.clear())
}; };
}, }
) )
} }
} }

View file

@ -1,6 +1,7 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
use std::fmt::{Display, Error, Formatter}; use std::fmt::{Display, Error, Formatter};
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
// --- elements --- // --- elements ---
@ -16,7 +17,7 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect
center_y / radius, center_y / radius,
center_z / radius, center_z / radius,
0.5 / radius, 0.5 / radius,
0.5 * (center_norm_sq / radius - radius), 0.5 * (center_norm_sq / radius - radius)
]) ])
} }
@ -30,7 +31,7 @@ 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_y,
norm_sp * dir_z, norm_sp * dir_z,
0.5 * curv, 0.5 * curv,
off * (1.0 + 0.5 * off * curv), off * (1.0 + 0.5 * off * curv)
]) ])
} }
@ -49,23 +50,57 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
rep.scale_mut(0.5 / rep[3]); rep.scale_mut(0.5 / rep[3]);
} }
// 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<f64>, half_curv: f64) {
// set the sphere's half-curvature to the desired value
rep[3] = half_curv;
// restore normalization by contracting toward the curvature axis
const SIZE_THRESHOLD: f64 = 1e-9;
let half_q_lt = -2.0 * half_curv * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let mut spatial = rep.fixed_rows_mut::<3>(0);
let q_sp = spatial.norm_squared();
if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD {
spatial.copy_from_slice(
&[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()]
);
} else {
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
spatial.scale_mut(1.0 / scaling);
rep[4] /= scaling;
}
/* DEBUG */
// verify normalization
let rep_for_debug = rep.clone();
console::log_1(&JsValue::from(
format!(
"Sphere self-product after curvature change: {}",
rep_for_debug.dot(&(&*Q * &rep_for_debug))
)
));
}
// --- partial matrices --- // --- partial matrices ---
pub struct MatrixEntry { pub struct MatrixEntry {
index: (usize, usize), index: (usize, usize),
value: f64, value: f64
} }
pub struct PartialMatrix(Vec<MatrixEntry>); pub struct PartialMatrix(Vec<MatrixEntry>);
impl PartialMatrix { impl PartialMatrix {
pub fn new() -> Self { pub fn new() -> PartialMatrix {
Self(Vec::<MatrixEntry>::new()) PartialMatrix(Vec::<MatrixEntry>::new())
} }
pub fn push(&mut self, row: usize, col: usize, value: f64) { pub fn push(&mut self, row: usize, col: usize, value: f64) {
let Self(entries) = self; let PartialMatrix(entries) = self;
entries.push(MatrixEntry { index: (row, col), value }); entries.push(MatrixEntry { index: (row, col), value: value });
} }
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
@ -114,7 +149,7 @@ impl IntoIterator for PartialMatrix {
type IntoIter = std::vec::IntoIter<Self::Item>; type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
let Self(entries) = self; let PartialMatrix(entries) = self;
entries.into_iter() entries.into_iter()
} }
} }
@ -135,26 +170,22 @@ impl<'a> IntoIterator for &'a PartialMatrix {
pub struct ConfigSubspace { pub struct ConfigSubspace {
assembly_dim: usize, assembly_dim: usize,
basis_std: Vec<DMatrix<f64>>, basis_std: Vec<DMatrix<f64>>,
basis_proj: Vec<DMatrix<f64>>, basis_proj: Vec<DMatrix<f64>>
} }
impl ConfigSubspace { impl ConfigSubspace {
pub fn zero(assembly_dim: usize) -> Self { pub fn zero(assembly_dim: usize) -> ConfigSubspace {
Self { ConfigSubspace {
assembly_dim, assembly_dim: assembly_dim,
basis_proj: Vec::new(), basis_proj: Vec::new(),
basis_std: Vec::new(), basis_std: Vec::new()
} }
} }
// approximate the kernel of a symmetric endomorphism of the configuration // approximate the kernel of a symmetric endomorphism of the configuration
// space for `assembly_dim` elements. we consider an eigenvector to be part // space for `assembly_dim` elements. we consider an eigenvector to be part
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
fn symmetric_kernel( fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
a: DMatrix<f64>,
proj_to_std: DMatrix<f64>,
assembly_dim: usize,
) -> Self {
// find a basis for the kernel. the basis is expressed in the projection // find a basis for the kernel. the basis is expressed in the projection
// coordinates, and it's orthonormal with respect to the projection // coordinates, and it's orthonormal with respect to the projection
// inner product // inner product
@ -168,13 +199,20 @@ impl ConfigSubspace {
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().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 // express the basis in the standard coordinates
let basis_std = proj_to_std * &basis_proj; let basis_std = proj_to_std * &basis_proj;
const ELEMENT_DIM: usize = 5; const ELEMENT_DIM: usize = 5;
const UNIFORM_DIM: usize = 4; const UNIFORM_DIM: usize = 4;
Self { ConfigSubspace {
assembly_dim, assembly_dim: assembly_dim,
basis_std: basis_std.column_iter().map( basis_std: basis_std.column_iter().map(
|v| Into::<DMatrix<f64>>::into( |v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
@ -184,7 +222,7 @@ impl ConfigSubspace {
|v| Into::<DMatrix<f64>>::into( |v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
) )
).collect(), ).collect()
} }
} }
@ -218,18 +256,18 @@ pub struct DescentHistory {
pub config: Vec<DMatrix<f64>>, pub config: Vec<DMatrix<f64>>,
pub scaled_loss: Vec<f64>, pub scaled_loss: Vec<f64>,
pub neg_grad: Vec<DMatrix<f64>>, pub neg_grad: Vec<DMatrix<f64>>,
pub hess_eigvals: Vec<DVector<f64>>, pub min_eigval: Vec<f64>,
pub base_step: Vec<DMatrix<f64>>, pub base_step: Vec<DMatrix<f64>>,
pub backoff_steps: Vec<i32>, pub backoff_steps: Vec<i32>
} }
impl DescentHistory { impl DescentHistory {
pub fn new() -> Self { fn new() -> DescentHistory {
Self { DescentHistory {
config: Vec::<DMatrix<f64>>::new(), config: Vec::<DMatrix<f64>>::new(),
scaled_loss: Vec::<f64>::new(), scaled_loss: Vec::<f64>::new(),
neg_grad: Vec::<DMatrix<f64>>::new(), neg_grad: Vec::<DMatrix<f64>>::new(),
hess_eigvals: Vec::<DVector<f64>>::new(), min_eigval: Vec::<f64>::new(),
base_step: Vec::<DMatrix<f64>>::new(), base_step: Vec::<DMatrix<f64>>::new(),
backoff_steps: Vec::<i32>::new(), backoff_steps: Vec::<i32>::new(),
} }
@ -245,21 +283,21 @@ pub struct ConstraintProblem {
} }
impl ConstraintProblem { impl ConstraintProblem {
pub fn new(element_count: usize) -> Self { pub fn new(element_count: usize) -> ConstraintProblem {
const ELEMENT_DIM: usize = 5; const ELEMENT_DIM: usize = 5;
Self { ConstraintProblem {
gram: PartialMatrix::new(), gram: PartialMatrix::new(),
frozen: PartialMatrix::new(), frozen: PartialMatrix::new(),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count), guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count)
} }
} }
#[cfg(feature = "dev")] #[cfg(feature = "dev")]
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self { pub fn from_guess(guess_columns: &[DVector<f64>]) -> ConstraintProblem {
Self { ConstraintProblem {
gram: PartialMatrix::new(), gram: PartialMatrix::new(),
frozen: PartialMatrix::new(), frozen: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns), guess: DMatrix::from_columns(guess_columns)
} }
} }
} }
@ -273,21 +311,25 @@ lazy_static! {
0.0, 1.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, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, -2.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 { struct SearchState {
config: DMatrix<f64>, config: DMatrix<f64>,
err_proj: DMatrix<f64>, err_proj: DMatrix<f64>,
loss: f64, loss: f64
} }
impl SearchState { impl SearchState {
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> Self { fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState {
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
let loss = err_proj.norm_squared(); let loss = err_proj.norm_squared();
Self { config, err_proj, loss } SearchState {
config: config,
err_proj: err_proj,
loss: loss
}
} }
} }
@ -314,7 +356,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0], curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1], 0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2], 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 { } else {
// `v` represents a sphere. the normalization condition says that the // `v` represents a sphere. the normalization condition says that the
@ -323,7 +365,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0], curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1], 0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2], 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 +378,7 @@ fn seek_better_config(
base_target_improvement: f64, base_target_improvement: f64,
min_efficiency: f64, min_efficiency: f64,
backoff: f64, backoff: f64,
max_backoff_steps: i32, max_backoff_steps: i32
) -> Option<(SearchState, i32)> { ) -> Option<(SearchState, i32)> {
let mut rate = 1.0; let mut rate = 1.0;
for backoff_steps in 0..max_backoff_steps { for backoff_steps in 0..max_backoff_steps {
@ -351,17 +393,6 @@ fn seek_better_config(
None None
} }
// a first-order neighborhood of a configuration
pub struct ConfigNeighborhood {
pub config: DMatrix<f64>,
pub nbhd: ConfigSubspace,
}
pub struct Realization {
pub result: Result<ConfigNeighborhood, String>,
pub history: DescentHistory,
}
// seek a matrix `config` that matches the partial matrix `problem.frozen` and // seek a matrix `config` that matches the partial matrix `problem.frozen` and
// has `config' * Q * config` matching the partial matrix `problem.gram`. start // has `config' * Q * config` matching the partial matrix `problem.gram`. start
// at `problem.guess`, set the frozen entries to their desired values, and then // at `problem.guess`, set the frozen entries to their desired values, and then
@ -373,30 +404,19 @@ pub fn realize_gram(
backoff: f64, backoff: f64,
reg_scale: f64, reg_scale: f64,
max_descent_steps: i32, max_descent_steps: i32,
max_backoff_steps: i32, max_backoff_steps: i32
) -> Realization { ) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
// destructure the problem data // destructure the problem data
let ConstraintProblem { gram, guess, frozen } = problem; let ConstraintProblem {
gram, guess, frozen
} = problem;
// start the descent history // start the descent history
let mut history = DescentHistory::new(); 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 // find the dimension of the search space
let element_dim = guess.nrows(); let element_dim = guess.nrows();
let assembly_dim = guess.ncols();
let total_dim = element_dim * assembly_dim; let total_dim = element_dim * assembly_dim;
// scale the tolerance // scale the tolerance
@ -437,12 +457,11 @@ pub fn realize_gram(
hess = DMatrix::from_columns(hess_cols.as_slice()); hess = DMatrix::from_columns(hess_cols.as_slice());
// regularize the Hessian // regularize the Hessian
let hess_eigvals = hess.symmetric_eigenvalues(); let min_eigval = hess.symmetric_eigenvalues().min();
let min_eigval = hess_eigvals.min();
if min_eigval <= 0.0 { if min_eigval <= 0.0 {
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); 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 // project the negative gradient and negative Hessian onto the
// orthogonal complement of the frozen subspace // orthogonal complement of the frozen subspace
@ -461,40 +480,30 @@ pub fn realize_gram(
if state.loss < tol { break; } if state.loss < tol { break; }
// compute the Newton step // compute the Newton step
/* TO DO */
/* /*
we should change our regularization to ensure that the Hessian is we need to either handle or eliminate the case where the minimum
is positive-definite, rather than just positive-semidefinite. ideally, eigenvalue of the Hessian is zero, so the regularized Hessian is
that would guarantee the success of the Cholesky decomposition--- singular. right now, this causes the Cholesky decomposition to return
although we'd still need the error-handling routine in case of `None`, leading to a panic when we unrap
numerical hiccups
*/ */
let hess_cholesky = match hess.clone().cholesky() { let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked);
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 = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
history.base_step.push(base_step.clone()); history.base_step.push(base_step.clone());
// use backtracking line search to find a better configuration // 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), gram, &state, &base_step, neg_grad.dot(&base_step),
min_efficiency, backoff, max_backoff_steps, min_efficiency, backoff, max_backoff_steps
) { ) {
Some((better_state, backoff_steps)) => {
state = better_state; state = better_state;
history.backoff_steps.push(backoff_steps); history.backoff_steps.push(backoff_steps);
} else { },
return Realization { None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history)
result: Err("Line search failed".to_string()),
history,
}; };
} }
} let success = state.loss < tol;
let result = if state.loss < tol { let tangent = if success {
// express the uniform basis in the standard basis // express the uniform basis in the standard basis
const UNIFORM_DIM: usize = 4; const UNIFORM_DIM: usize = 4;
let total_dim_unif = UNIFORM_DIM * assembly_dim; let total_dim_unif = UNIFORM_DIM * assembly_dim;
@ -507,13 +516,11 @@ pub fn realize_gram(
} }
// find the kernel of the Hessian. give it the uniform inner product // find the kernel of the Hessian. give it the uniform inner product
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim)
Ok(ConfigNeighborhood { config: state.config, nbhd: tangent })
} else { } else {
Err("Failed to reach target accuracy".to_string()) ConfigSubspace::zero(assembly_dim)
}; };
Realization { result, history } (state.config, tangent, success, history)
} }
// --- tests --- // --- tests ---
@ -532,12 +539,12 @@ pub mod examples {
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
// https://www.nippon.com/en/japan-topics/c12801/ // 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<f64>, ConfigSubspace, bool, DescentHistory) {
let mut problem = ConstraintProblem::from_guess( let mut problem = ConstraintProblem::from_guess(
[ [
sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, 0.0, 15.0),
sphere(0.0, 0.0, -9.0, 5.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( ].into_iter().chain(
(1..=6).map( (1..=6).map(
|k| { |k| {
@ -583,7 +590,7 @@ pub mod examples {
// set up a kaleidocycle, made of points with fixed distances between them, // set up a kaleidocycle, made of points with fixed distances between them,
// and find its tangent space // and find its tangent space
pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
const N_HINGES: usize = 6; const N_HINGES: usize = 6;
let mut problem = ConstraintProblem::from_guess( let mut problem = ConstraintProblem::from_guess(
(0..N_HINGES).step_by(2).flat_map( (0..N_HINGES).step_by(2).flat_map(
@ -596,7 +603,7 @@ pub mod examples {
point(0.0, 0.0, 0.0), point(0.0, 0.0, 0.0),
point(ang_hor.cos(), ang_hor.sin(), 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), point(x_vert, y_vert, 0.5)
] ]
} }
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
@ -639,15 +646,15 @@ mod tests {
MatrixEntry { index: (0, 0), value: 14.0 }, MatrixEntry { index: (0, 0), value: 14.0 },
MatrixEntry { index: (0, 2), value: 28.0 }, MatrixEntry { index: (0, 2), value: 28.0 },
MatrixEntry { index: (1, 1), value: 42.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::<f64>::from_row_slice(2, 3, &[ let config = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
4.0, 5.0, 6.0, 4.0, 5.0, 6.0
]); ]);
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[ let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
14.0, 2.0, 28.0, 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); assert_eq!(frozen.freeze(&config), expected_result);
} }
@ -658,15 +665,15 @@ mod tests {
MatrixEntry { index: (0, 0), value: 19.0 }, MatrixEntry { index: (0, 0), value: 19.0 },
MatrixEntry { index: (0, 2), value: 39.0 }, MatrixEntry { index: (0, 2), value: 39.0 },
MatrixEntry { index: (1, 1), value: 59.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::<f64>::from_row_slice(2, 3, &[ let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
4.0, 5.0, 6.0, 4.0, 5.0, 6.0
]); ]);
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[ let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
18.0, 0.0, 36.0, 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); assert_eq!(target.sub_proj(&attempt), expected_result);
} }
@ -684,7 +691,7 @@ mod tests {
DMatrix::from_columns(&[ DMatrix::from_columns(&[
sphere(1.0, 0.0, 0.0, a), 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), sphere(-0.5, -a, 0.0, a)
]) ])
}; };
let state = SearchState::from_config(&gram, config); let state = SearchState::from_config(&gram, config);
@ -698,7 +705,7 @@ mod tests {
fn frozen_entry_test() { fn frozen_entry_test() {
let mut problem = ConstraintProblem::from_guess(&[ let mut problem = ConstraintProblem::from_guess(&[
point(0.0, 0.0, 2.0), 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 j in 0..2 {
for k in j..2 { for k in j..2 {
@ -707,10 +714,10 @@ mod tests {
} }
problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 0, problem.guess[(3, 0)]);
problem.frozen.push(3, 1, 0.5); 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 &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 base_step in history.base_step.into_iter() {
for &MatrixEntry { index, .. } in &problem.frozen { for &MatrixEntry { index, .. } in &problem.frozen {
assert_eq!(base_step[index], 0.0); assert_eq!(base_step[index], 0.0);
@ -725,7 +732,7 @@ mod tests {
fn irisawa_hexlet_test() { fn irisawa_hexlet_test() {
// solve Irisawa's problem // solve Irisawa's problem
const SCALED_TOL: f64 = 1.0e-12; 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 // check against Irisawa's solution
let entry_tol = SCALED_TOL.sqrt(); let entry_tol = SCALED_TOL.sqrt();
@ -742,7 +749,7 @@ mod tests {
let mut problem = ConstraintProblem::from_guess(&[ let mut problem = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.0, -2.0), 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), sphere(0.0, 0.0, -1.0, 1.0)
]); ]);
for j in 0..3 { for j in 0..3 {
for k in j..3 { for k in j..3 {
@ -752,11 +759,11 @@ mod tests {
for n in 0..ELEMENT_DIM { for n in 0..ELEMENT_DIM {
problem.frozen.push(n, 0, problem.guess[(n, 0)]); 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 &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!(config, problem.guess);
assert_eq!(success, true);
assert_eq!(history.scaled_loss.len(), 1); assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of // list some motions that should form a basis for the tangent space of
@ -772,8 +779,8 @@ mod tests {
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[ DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
0.0, 0.0, 0.0, 0.0, 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, 0.0, 0.0, -0.5, 0.5
]), ])
]; ];
let tangent_motions_std = vec![ let tangent_motions_std = vec![
basis_matrix((0, 1), element_dim, assembly_dim), basis_matrix((0, 1), element_dim, assembly_dim),
@ -783,8 +790,8 @@ mod tests {
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[ DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
0.0, 0.0, 0.0, 0.00, 0.0, 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, 0.0, 0.0, -1.0, 0.25, 1.0
]), ])
]; ];
// confirm that the dimension of the tangent space is no greater than // confirm that the dimension of the tangent space is no greater than
@ -824,8 +831,8 @@ mod tests {
fn tangent_test_kaleidocycle() { fn tangent_test_kaleidocycle() {
// set up a kaleidocycle and find its tangent space // set up a kaleidocycle and find its tangent space
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(success, true);
assert_eq!(history.scaled_loss.len(), 1); assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of // list some motions that should form a basis for the tangent space of
@ -860,10 +867,10 @@ mod tests {
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), 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(&[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]), DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0])
] ]
} }
).collect::<Vec<_>>(), ).collect::<Vec<_>>()
]; ];
let tangent_motions_std = tangent_motions_unif.iter().map( let tangent_motions_std = tangent_motions_unif.iter().map(
|motion| DMatrix::from_columns( |motion| DMatrix::from_columns(
@ -896,7 +903,7 @@ mod tests {
0.0, 1.0, 0.0, 0.0, dis[1], 0.0, 1.0, 0.0, 0.0, dis[1],
0.0, 0.0, 1.0, 0.0, dis[2], 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(), 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 +915,16 @@ mod tests {
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let mut problem_orig = ConstraintProblem::from_guess(&[ 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), sphere(0.0, 0.0, -0.5, 1.0)
]); ]);
problem_orig.gram.push_sym(0, 0, 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(1, 1, 1.0);
problem_orig.gram.push_sym(0, 1, 0.5); 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 &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!(config_orig, problem_orig.guess);
assert_eq!(success_orig, true);
assert_eq!(history_orig.scaled_loss.len(), 1); assert_eq!(history_orig.scaled_loss.len(), 1);
// find another pair of spheres that meet at 120°. we'll think of this // find another pair of spheres that meet at 120°. we'll think of this
@ -926,19 +933,19 @@ mod tests {
let a = 0.5 * FRAC_1_SQRT_2; let a = 0.5 * FRAC_1_SQRT_2;
DMatrix::from_columns(&[ 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), sphere(-a, 0.0, 7.0 - a, 1.0)
]) ])
}; };
let problem_tfm = ConstraintProblem { let problem_tfm = ConstraintProblem {
gram: problem_orig.gram, gram: problem_orig.gram,
frozen: problem_orig.frozen,
guess: guess_tfm, 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 &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!(config_tfm, problem_tfm.guess);
assert_eq!(success_tfm, true);
assert_eq!(history_tfm.scaled_loss.len(), 1); assert_eq!(history_tfm.scaled_loss.len(), 1);
// project a nudge to the tangent space of the solution variety at the // project a nudge to the tangent space of the solution variety at the
@ -960,7 +967,7 @@ mod tests {
0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 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, 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 transl = translation(Vector3::new(0.0, 0.0, 7.0));
let motion_proj_tfm = transl * rot * motion_orig_proj; let motion_proj_tfm = transl * rot * motion_orig_proj;

View file

@ -1,6 +1,8 @@
mod add_remove;
mod assembly; mod assembly;
mod components; mod display;
mod engine; mod engine;
mod outline;
mod specified; mod specified;
#[cfg(test)] #[cfg(test)]
@ -9,25 +11,22 @@ mod tests;
use std::{collections::BTreeSet, rc::Rc}; use std::{collections::BTreeSet, rc::Rc};
use sycamore::prelude::*; use sycamore::prelude::*;
use add_remove::AddRemove;
use assembly::{Assembly, Element}; use assembly::{Assembly, Element};
use components::{ use display::Display;
add_remove::AddRemove, use outline::Outline;
diagnostics::Diagnostics,
display::Display,
outline::Outline,
};
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
assembly: Assembly, assembly: Assembly,
selection: Signal<BTreeSet<Rc<dyn Element>>>, selection: Signal<BTreeSet<Rc<dyn Element>>>
} }
impl AppState { impl AppState {
fn new() -> Self { fn new() -> AppState {
Self { AppState {
assembly: Assembly::new(), assembly: Assembly::new(),
selection: create_signal(BTreeSet::default()), selection: create_signal(BTreeSet::default())
} }
} }
@ -61,7 +60,6 @@ fn main() {
div(id="sidebar") { div(id="sidebar") {
AddRemove {} AddRemove {}
Outline {} Outline {}
Diagnostics {}
} }
Display {} Display {}
} }

View file

@ -1,7 +1,11 @@
use itertools::Itertools; use itertools::Itertools;
use std::rc::Rc; use std::rc::Rc;
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use web_sys::{
KeyboardEvent,
MouseEvent,
wasm_bindgen::JsCast
};
use crate::{ use crate::{
AppState, AppState,
@ -9,7 +13,7 @@ use crate::{
Element, Element,
HalfCurvatureRegulator, HalfCurvatureRegulator,
InversiveDistanceRegulator, InversiveDistanceRegulator,
Regulator, Regulator
}, },
specified::SpecifiedValue specified::SpecifiedValue
}; };
@ -67,8 +71,8 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
Ok(set_pt) => { Ok(set_pt) => {
set_point.set(set_pt); set_point.set(set_pt);
true true
}, }
Err(_) => false, Err(_) => false
} }
) )
}, },
@ -76,10 +80,10 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
move |event: KeyboardEvent| { move |event: KeyboardEvent| {
match event.key().as_str() { match event.key().as_str() {
"Escape" => reset_value(), "Escape" => reset_value(),
_ => (), _ => ()
}
} }
} }
},
) )
} }
} }
@ -175,7 +179,7 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
.unchecked_into::<web_sys::Element>() .unchecked_into::<web_sys::Element>()
.remove_attribute("open"); .remove_attribute("open");
}, },
_ => (), _ => ()
} }
} }
} }

View file

@ -13,12 +13,12 @@ use std::num::ParseFloatError;
#[readonly::make] #[readonly::make]
pub struct SpecifiedValue { pub struct SpecifiedValue {
pub spec: String, pub spec: String,
pub value: Option<f64>, pub value: Option<f64>
} }
impl SpecifiedValue { impl SpecifiedValue {
pub fn from_empty_spec() -> Self { pub fn from_empty_spec() -> SpecifiedValue {
Self { spec: String::new(), value: None } SpecifiedValue { spec: String::new(), value: None }
} }
pub fn is_present(&self) -> bool { pub fn is_present(&self) -> bool {
@ -34,10 +34,10 @@ impl TryFrom<String> for SpecifiedValue {
fn try_from(spec: String) -> Result<Self, Self::Error> { fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() { if spec.is_empty() {
Ok(Self::from_empty_spec()) Ok(SpecifiedValue::from_empty_spec())
} else { } else {
spec.parse::<f64>().map( spec.parse::<f64>().map(
|value| Self { spec, value: Some(value) } |value| SpecifiedValue { spec: spec, value: Some(value) }
) )
} }
} }

5
deploy/.gitignore vendored
View file

@ -1,5 +0,0 @@
/dyna3.zip
/dyna3/index.html
/dyna3/dyna3-*.js
/dyna3/dyna3-*.wasm
/dyna3/main-*.css

View file

@ -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"

View file

@ -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