diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 55e8686..4f75c45 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -20,6 +20,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "approx" version = "0.5.1" @@ -35,6 +50,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -68,6 +98,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charming" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09" +dependencies = [ + "handlebars", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_with", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -78,10 +137,122 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyna3" version = "0.1.0" dependencies = [ + "charming", "console_error_panic_hook", "dyna3", "itertools", @@ -106,6 +277,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -117,6 +304,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -127,6 +336,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html-escape" version = "0.2.13" @@ -136,6 +351,47 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -143,7 +399,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -155,6 +412,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.70" @@ -192,6 +455,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minicov" version = "0.3.5" @@ -248,6 +517,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -257,6 +532,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -289,6 +579,57 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -363,6 +704,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.2" @@ -387,6 +734,90 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -421,14 +852,20 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "sycamore" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ - "hashbrown", - "indexmap", + "hashbrown 0.14.5", + "indexmap 2.5.0", "paste", "sycamore-core", "sycamore-macro", @@ -444,7 +881,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "paste", "sycamore-reactive", ] @@ -506,21 +943,78 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -677,6 +1171,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 6932b72..1230b47 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -17,6 +17,9 @@ nalgebra = "0.33.0" readonly = "0.2.12" sycamore = "0.9.1" +# We use Charming to help display engine diagnostics +charming = { version = "0.5.1", features = ["wasm"] } + # The `console_error_panic_hook` crate provides better debugging of panics by # 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 diff --git a/app-proto/examples/common/print.rs b/app-proto/examples/common/print.rs new file mode 100644 index 0000000..2aa6a39 --- /dev/null +++ b/app-proto/examples/common/print.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use nalgebra::DMatrix; + +use dyna3::engine::{Q, DescentHistory, Realization}; + +pub fn title(title: &str) { + println!("─── {title} ───"); +} + +pub fn realization_diagnostics(realization: &Realization) { + let Realization { result, history } = realization; + println!(); + if let Err(ref message) = result { + println!("❌️ {message}"); + } else { + println!("✅️ Target accuracy achieved!"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); +} + +pub fn gram_matrix(config: &DMatrix) { + println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); +} + +pub fn config(config: &DMatrix) { + println!("\nConfiguration:{}", config.to_string().trim_end()); +} + +pub fn loss_history(history: &DescentHistory) { + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 639a494..0d710ff 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,25 +1,23 @@ -use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + let realization = realize_irisawa_hexlet(SCALED_TOL); + print::title("Irisawa hexlet"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, .. }) = realization.result { + // print the diameters of the chain spheres println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } + + // print the completed Gram matrix + print::gram_matrix(&config); } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 2779ab1..7ca1f97 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,30 +1,32 @@ +#[path = "common/print.rs"] +mod print; + use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{Q, examples::realize_kaleidocycle}; +use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); + let realization = realize_kaleidocycle(SCALED_TOL); + print::title("Kaleidocycle"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { + // print the completed Gram matrix and the realized configuration + print::gram_matrix(&config); + print::config(&config); + + // find the kaleidocycle's twist motion by projecting onto the tangent + // space + const N_POINTS: usize = 12; + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - - // find the kaleidocycle's twist motion by projecting onto the tangent space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1) - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - print!("Twist motion:{}", normalization * twist_motion); } \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 880d7b0..89dee76 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,4 +1,13 @@ -use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + point, + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess(&[ @@ -11,21 +20,14 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Point on a sphere"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); + print::config(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 3f3cc44..aa5a105 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,4 +1,12 @@ -use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -14,20 +22,13 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Three spheres"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/index.html b/app-proto/index.html index 92238f4..4fbe52f 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,6 +6,12 @@ + + + diff --git a/app-proto/main.css b/app-proto/main.css index d56784f..7981285 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -18,6 +18,17 @@ body { font-family: 'Fira Sans', sans-serif; } +.invalid { + color: var(--text-invalid); +} + +.status { + width: 20px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + /* sidebar */ #sidebar { @@ -138,6 +149,7 @@ details[open]:has(li) .element-switch::after { } .regulator-input { + margin-right: 4px; color: inherit; background-color: inherit; border: 1px solid var(--border); @@ -159,22 +171,56 @@ details[open]:has(li) .element-switch::after { border-color: var(--border-invalid); } -.status { - width: 20px; - padding-left: 4px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } +/* diagnostics */ + +#diagnostics { + margin: 10px; +} + +#diagnostics-bar { + display: flex; +} + +#realization-status { + display: flex; + flex-grow: 1; +} + +#realization-status .status { + margin-right: 4px; +} + +#realization-status :not(.status) { + flex-grow: 1; +} + +#realization-status .status::after { + content: '✓'; +} + +#realization-status.invalid .status::after { + content: '⚠'; +} + +.diagnostics-panel { + margin-top: 10px; + min-height: 180px; +} + +.diagnostics-chart { + background-color: var(--display-background); + border: 1px solid var(--border); + border-radius: 8px; +} + /* display */ -canvas { +#display { float: left; margin-left: 20px; margin-top: 20px; @@ -183,7 +229,7 @@ canvas { border-radius: 16px; } -canvas:focus { +#display:focus { border-color: var(--border-focus-dark); outline: none; } diff --git a/app-proto/run-examples b/app-proto/run-examples deleted file mode 100755 index 52173b0..0000000 --- a/app-proto/run-examples +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# run all Cargo examples, as described here: -# -# Karol Kuczmarski. "Add examples to your Rust libraries" -# http://xion.io/post/code/rust-examples.html -# - -cargo run --example irisawa-hexlet -cargo run --example three-spheres -cargo run --example point-on-sphere -cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/run-examples.sh b/app-proto/run-examples.sh new file mode 100644 index 0000000..861addf --- /dev/null +++ b/app-proto/run-examples.sh @@ -0,0 +1,20 @@ +# 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)/Cargo.toml" + +# set up the command that runs each example +RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" + +# run the examples +$RUN_EXAMPLE irisawa-hexlet; echo +$RUN_EXAMPLE three-spheres; echo +$RUN_EXAMPLE point-on-sphere; echo +$RUN_EXAMPLE kaleidocycle \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index f3bbc97..d737c79 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -3,8 +3,9 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ - engine, AppState, + engine, + engine::DescentHistory, assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; @@ -195,6 +196,7 @@ pub fn AddRemove() -> View { 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 diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 6c91fc0..c3b0c6b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -23,8 +23,11 @@ use crate::{ project_sphere_to_normalized, realize_gram, sphere, + ConfigNeighborhood, ConfigSubspace, - ConstraintProblem + ConstraintProblem, + DescentHistory, + Realization }, outline::OutlineItem, specified::SpecifiedValue @@ -547,7 +550,11 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal>> + pub elements_by_id: Signal>>, + + // realization diagnostics + pub realization_status: Signal>, + pub descent_history: Signal } impl Assembly { @@ -556,7 +563,9 @@ impl Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()) + elements_by_id: create_signal(BTreeMap::default()), + realization_status: create_signal(Ok(())), + descent_history: create_signal(DescentHistory::new()) } } @@ -687,31 +696,49 @@ impl Assembly { console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ - // report the outcome of the search - if success { - console_log!("Target accuracy achieved!") + // report the outcome of the search in the browser console + if let Err(ref message) = result { + console_log!("❌️ {message}"); } else { - console_log!("Failed to reach target accuracy") + console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); - console_log!("Tangent dimension: {}", tangent.dim()); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { - // read out the solution - for elt in self.elements.get_clone_untracked() { - elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) - ); + // report the loss history + self.descent_history.set(history); + + match result { + Ok(ConfigNeighborhood { config, nbhd: tangent }) => { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + + // report the realization status + self.realization_status.set(Ok(())); + + // read out the solution + for elt in self.elements.get_clone_untracked() { + elt.representation().update( + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) + ); + } + + // save the tangent space + self.tangent.set_silent(tangent); + }, + Err(message) => { + // report the realization status. the `Err(message)` we're + // setting the status to has a different type than the + // `Err(message)` we received from the match: we're changing the + // `Ok` type from `Realization` to `()` + self.realization_status.set(Err(message)) } - - // save the tangent space - self.tangent.set_silent(tangent); } } diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs new file mode 100644 index 0000000..a2f090a --- /dev/null +++ b/app-proto/src/diagnostics.rs @@ -0,0 +1,258 @@ +use charming::{ + Chart, + WasmRenderer, + component::{Axis, DataZoom, Grid}, + element::{AxisType, Symbol}, + series::{Line, Scatter}, +}; +use sycamore::prelude::*; + +use crate::AppState; + +#[derive(Clone)] +struct DiagnosticsState { + active_tab: Signal +} + +impl DiagnosticsState { + fn new(initial_tab: String) -> DiagnosticsState { + DiagnosticsState { + active_tab: create_signal(initial_tab) + } + } +} + +// a realization status indicator +#[component] +fn RealizationStatus() -> View { + let state = use_context::(); + let realization_status = state.assembly.realization_status; + view! { + div( + id="realization-status", + class=realization_status.with( + |status| match status { + Ok(_) => "", + Err(_) => "invalid" + } + ) + ) { + div(class="status") + div { + (realization_status.with( + |status| match status { + Ok(_) => "Target accuracy achieved".to_string(), + Err(message) => message.clone() + } + )) + } + } + } +} + +fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { + vec![ + Some(step as f64), + if value == 0.0 { None } else { Some(value.abs().log10()) } + ] +} + +// the loss history from the last realization +#[component] +fn LossHistory() -> View { + const CONTAINER_ID: &str = "loss-history"; + let state = use_context::(); + let renderer = WasmRenderer::new_opt(None, Some(178)); + + on_mount(move || { + create_effect(move || { + // get the loss history + let scaled_loss: Vec<_> = state.assembly.descent_history.with( + |history| history.scaled_loss + .iter() + .enumerate() + .map(|(step, &loss)| (step, loss)) + .map(into_log10_time_point) + .collect() + ); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let scaled_loss_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let scaled_loss_series = Line::new().data( + if scaled_loss.len() > 0 { + scaled_loss + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(scaled_loss_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(scaled_loss_series); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +// the spectrum of the Hessian during the last realization +#[component] +fn SpectrumHistory() -> View { + const CONTAINER_ID: &str = "spectrum-history"; + let state = use_context::(); + let renderer = WasmRenderer::new(478, 178); + + on_mount(move || { + create_effect(move || { + // get the spectrum of the Hessian at each step, split into its + // positive, negative, and strictly-zero parts + let ( + hess_eigvals_zero, + hess_eigvals_nonzero + ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( + |history| history.hess_eigvals + .iter() + .enumerate() + .map( + |(step, eigvals)| eigvals.iter().map( + move |&val| (step, val) + ) + ) + .flatten() + .partition(|&(_, val)| val == 0.0) + ); + let zero_level = hess_eigvals_nonzero + .iter() + .map(|(_, val)| val.abs()) + .reduce(f64::min) + .map(|val| 0.1 * val) + .unwrap_or(1.0); + let ( + hess_eigvals_pos, + hess_eigvals_neg + ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero + .into_iter() + .partition(|&(_, val)| val > 0.0); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let eigval_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let eigval_series_pos = Scatter::new() + .symbol_size(4.5) + .data( + if hess_eigvals_pos.len() > 0 { + hess_eigvals_pos + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_neg = Scatter::new() + .symbol(Symbol::Diamond) + .symbol_size(6.0) + .data( + if hess_eigvals_neg.len() > 0 { + hess_eigvals_neg + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_zero = Scatter::new() + .symbol(Symbol::Triangle) + .symbol_size(5.0) + .data( + if hess_eigvals_zero.len() > 0 { + hess_eigvals_zero + .into_iter() + .map(|(step, _)| (step, zero_level)) + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(eigval_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(eigval_series_pos) + .series(eigval_series_neg) + .series(eigval_series_zero); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +#[component(inline_props)] +fn DiagnosticsPanel(name: &'static str, children: Children) -> View { + let diagnostics_state = use_context::(); + view! { + div( + class="diagnostics-panel", + "hidden"=diagnostics_state.active_tab.with( + |active_tab| { + if active_tab == name { + None + } else { + Some("") + } + } + ) + ) { + (children) + } + } +} + +#[component] +pub fn Diagnostics() -> View { + let diagnostics_state = DiagnosticsState::new("loss".to_string()); + let active_tab = diagnostics_state.active_tab.clone(); + provide_context(diagnostics_state); + + view! { + div(id="diagnostics") { + div(id="diagnostics-bar") { + RealizationStatus {} + select(bind:value=active_tab) { + option(value="loss") { "Loss" } + option(value="spectrum") { "Spectrum" } + } + } + DiagnosticsPanel(name="loss") { LossHistory {} } + DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } + } + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 69a3659..1646c4e 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -806,6 +806,7 @@ pub fn Display() -> View { // again canvas( ref=display, + id="display", width="600", height="600", tabindex="0", diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index c5d7b00..e6ffa25 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -256,18 +256,18 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub min_eigval: Vec, + pub hess_eigvals: Vec::>, pub base_step: Vec>, pub backoff_steps: Vec } impl DescentHistory { - fn new() -> DescentHistory { + pub fn new() -> DescentHistory { DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), - min_eigval: Vec::::new(), + hess_eigvals: Vec::>::new(), base_step: Vec::>::new(), backoff_steps: Vec::::new(), } @@ -393,6 +393,17 @@ fn seek_better_config( None } +// a first-order neighborhood of a configuration +pub struct ConfigNeighborhood { + pub config: DMatrix, + pub nbhd: ConfigSubspace +} + +pub struct Realization { + pub result: Result, + pub history: DescentHistory +} + // seek a matrix `config` that matches the partial matrix `problem.frozen` and // has `config' * Q * config` matching the partial matrix `problem.gram`. start // at `problem.guess`, set the frozen entries to their desired values, and then @@ -405,7 +416,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { +) -> Realization { // destructure the problem data let ConstraintProblem { gram, guess, frozen @@ -457,11 +468,12 @@ pub fn realize_gram( hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian - let min_eigval = hess.symmetric_eigenvalues().min(); + let hess_eigvals = hess.symmetric_eigenvalues(); + let min_eigval = hess_eigvals.min(); if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } - history.min_eigval.push(min_eigval); + history.hess_eigvals.push(hess_eigvals); // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace @@ -480,30 +492,40 @@ pub fn realize_gram( if state.loss < tol { break; } // compute the Newton step + /* TO DO */ /* - we need to either handle or eliminate the case where the minimum - eigenvalue of the Hessian is zero, so the regularized Hessian is - singular. right now, this causes the Cholesky decomposition to return - `None`, leading to a panic when we unrap + we should change our regularization to ensure that the Hessian is + is positive-definite, rather than just positive-semidefinite. ideally, + that would guarantee the success of the Cholesky decomposition--- + although we'd still need the error-handling routine in case of + numerical hiccups */ - let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); + let hess_cholesky = match hess.clone().cholesky() { + Some(cholesky) => cholesky, + None => return Realization { + result: Err("Cholesky decomposition failed".to_string()), + history + } + }; + let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); // use backtracking line search to find a better configuration - match seek_better_config( + if let Some((better_state, backoff_steps)) = seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), min_efficiency, backoff, max_backoff_steps ) { - Some((better_state, backoff_steps)) => { - state = better_state; - history.backoff_steps.push(backoff_steps); - }, - None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) + state = better_state; + history.backoff_steps.push(backoff_steps); + } else { + return Realization { + result: Err("Line search failed".to_string()), + history + } }; } - let success = state.loss < tol; - let tangent = if success { + let result = if state.loss < tol { // express the uniform basis in the standard basis const UNIFORM_DIM: usize = 4; let total_dim_unif = UNIFORM_DIM * assembly_dim; @@ -516,11 +538,13 @@ pub fn realize_gram( } // find the kernel of the Hessian. give it the uniform inner product - ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) + let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); + + Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) } else { - ConfigSubspace::zero(assembly_dim) + Err("Failed to reach target accuracy".to_string()) }; - (state.config, tangent, success, history) + Realization { result, history } } // --- tests --- @@ -539,7 +563,7 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -590,7 +614,7 @@ pub mod examples { // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space - pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -714,10 +738,10 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let (config, _, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(success, true); + let config = result.unwrap().config; for base_step in history.base_step.into_iter() { for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); @@ -732,7 +756,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -759,11 +783,11 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(config, problem.guess); - assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -831,8 +855,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - assert_eq!(success, true); + let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -920,11 +944,11 @@ mod tests { problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + let Realization { result: result_orig, history: history_orig } = realize_gram( &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); - assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); // find another pair of spheres that meet at 120°. we'll think of this @@ -941,11 +965,11 @@ mod tests { guess: guess_tfm, frozen: problem_orig.frozen }; - let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + let Realization { result: result_tfm, history: history_tfm } = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); - assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); // project a nudge to the tangent space of the solution variety at the diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index b76859a..f905c46 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,5 +1,6 @@ mod add_remove; mod assembly; +mod diagnostics; mod display; mod engine; mod outline; @@ -13,6 +14,7 @@ use sycamore::prelude::*; use add_remove::AddRemove; use assembly::{Assembly, Element}; +use diagnostics::Diagnostics; use display::Display; use outline::Outline; @@ -60,6 +62,7 @@ fn main() { div(id="sidebar") { AddRemove {} Outline {} + Diagnostics {} } Display {} }