Compare commits

...

12 commits

Author SHA1 Message Date
Aaron Fenyes
68d6cc1645 Add a strictly-zero series to the spectrum history
All checks were successful
/ test (pull_request) Successful in 3m37s
2025-06-26 22:42:02 -07:00
Aaron Fenyes
e877985202 Map diagnostics chart series to log scale by hand
This works around problems with the way ECharts does scaling and data
zoom for logarithmic axes.
2025-06-26 22:42:02 -07:00
Aaron Fenyes
2688b76678 Switch to log scale for diagnostics charts
Split the spectrum of the Hessian into its positive and negative parts
and plot them on the same logarithmic axis. The data zoom controls are
still linearly scaled, as discussed in ECharts issue #20927

  https://github.com/apache/echarts/issues/20927
2025-06-26 22:42:02 -07:00
Aaron Fenyes
af28e885bb Add data zoom controls to the diagnostics charts 2025-06-26 22:42:02 -07:00
Aaron Fenyes
de844cb63b Add a spectrum history panel
This introduces a framework for adding more diagnostics panels.
2025-06-26 22:42:02 -07:00
Aaron Fenyes
652cdd1573 Put a border around the loss history chart 2025-06-26 22:42:02 -07:00
Aaron Fenyes
28cf19cd26 Fix the display's focus indicator 2025-06-26 22:42:02 -07:00
Aaron Fenyes
c3c665f35c Let the Cholesky decomposition fail gracefully 2025-06-26 22:42:02 -07:00
Aaron Fenyes
402f5609c0 Add a realization status indicator 2025-06-26 22:42:02 -07:00
Aaron Fenyes
0b333ac00d Show the loss history from the last realization
This introduces a dependency on the Charming crate, which we use to plot
the loss history, and the ECharts JavaScript library, which Charming
depends on.

Now that there's more than one canvas on the page, we have to pick out
the display by ID rather than by element type in our style sheet.
2025-06-26 22:42:02 -07:00
Aaron Fenyes
679c421d04 Encapsulate realization results
In the process, spruce up our realization diagnostics logging and factor
out some of the repetitive code in the examples, because we're already
changing those parts of the code to adapt them to the new encapsulation.

This commit changes the example output format. I've checked by hand that
the output is rearranged but not meaningfully changed.
2025-06-26 22:42:02 -07:00
4cb3262555 chore: Update Sycamore to 0.9.1 (#91)
All checks were successful
/ test (push) Successful in 2m31s
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: #91
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-06-26 22:11:02 +00:00
17 changed files with 1135 additions and 157 deletions

592
app-proto/Cargo.lock generated
View file

@ -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"
@ -422,13 +853,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "sycamore"
version = "0.9.0-beta.3"
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd"
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",
@ -440,20 +877,20 @@ dependencies = [
[[package]]
name = "sycamore-core"
version = "0.9.0-beta.3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a"
checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187"
dependencies = [
"hashbrown",
"hashbrown 0.14.5",
"paste",
"sycamore-reactive",
]
[[package]]
name = "sycamore-macro"
version = "0.9.0-beta.3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014"
checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137"
dependencies = [
"once_cell",
"proc-macro2",
@ -465,20 +902,21 @@ dependencies = [
[[package]]
name = "sycamore-reactive"
version = "0.9.0-beta.3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1"
checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add"
dependencies = [
"paste",
"slotmap",
"smallvec",
"wasm-bindgen",
]
[[package]]
name = "sycamore-view-parser"
version = "0.9.0-beta.3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b"
checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033"
dependencies = [
"proc-macro2",
"quote",
@ -487,9 +925,9 @@ dependencies = [
[[package]]
name = "sycamore-web"
version = "0.9.0-beta.3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475"
checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd"
dependencies = [
"html-escape",
"js-sys",
@ -505,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"
@ -676,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"

View file

@ -15,7 +15,10 @@ js-sys = "0.3.70"
lazy_static = "1.5.0"
nalgebra = "0.33.0"
readonly = "0.2.12"
sycamore = "0.9.0-beta.3"
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

View file

@ -0,0 +1,36 @@
#![allow(dead_code)]
use nalgebra::DMatrix;
use dyna3::engine::{Q, DescentHistory, RealizationResult};
pub fn print_title(title: &str) {
println!("─── {title} ───");
}
pub fn print_realization_diagnostics(realization_result: &RealizationResult) {
let RealizationResult { result, history } = realization_result;
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 print_gram_matrix(config: &DMatrix<f64>) {
println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end());
}
pub fn print_config(config: &DMatrix<f64>) {
println!("\nConfiguration:{}", config.to_string().trim_end());
}
pub fn print_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,25 +1,28 @@
use dyna3::engine::{Q, examples::realize_irisawa_hexlet};
mod common;
use common::{
print_gram_matrix,
print_loss_history,
print_realization_diagnostics,
print_title
};
use dyna3::engine::{Realization, 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_result = realize_irisawa_hexlet(SCALED_TOL);
print_title("Irisawa hexlet");
print_realization_diagnostics(&realization_result);
if let Ok(Realization { config, .. }) = realization_result.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_result.history);
}

View file

@ -1,30 +1,37 @@
mod common;
use nalgebra::{DMatrix, DVector};
use dyna3::engine::{Q, examples::realize_kaleidocycle};
use common::{
print_config,
print_gram_matrix,
print_realization_diagnostics,
print_title
};
use dyna3::engine::{Realization, 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_result = realize_kaleidocycle(SCALED_TOL);
print_title("Kaleidocycle");
print_realization_diagnostics(&realization_result);
if let Ok(Realization { config, tangent }) = realization_result.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);
}

View file

@ -1,4 +1,19 @@
use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem};
mod common;
use common::{
print_config,
print_gram_matrix,
print_loss_history,
print_realization_diagnostics,
print_title
};
use dyna3::engine::{
point,
realize_gram,
sphere,
ConstraintProblem,
Realization
};
fn main() {
let mut problem = ConstraintProblem::from_guess(&[
@ -11,21 +26,14 @@ fn main() {
}
}
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
println!();
let (config, _, success, history) = realize_gram(
let realization_result = 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_result);
if let Ok(Realization{ config, .. }) = realization_result.result {
print_gram_matrix(&config);
print_config(&config);
}
print_loss_history(&realization_result.history);
}

View file

@ -1,4 +1,12 @@
use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem};
mod common;
use common::{
print_gram_matrix,
print_loss_history,
print_realization_diagnostics,
print_title
};
use dyna3::engine::{realize_gram, sphere, ConstraintProblem, Realization};
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_result = 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_result);
if let Ok(Realization{ config, .. }) = realization_result.result {
print_gram_matrix(&config);
}
print_loss_history(&realization_result.history);
}

View file

@ -6,6 +6,12 @@
<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=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>
<body></body>
</html>

View file

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

View file

@ -6,7 +6,7 @@
# 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 irisawa-hexlet; echo
cargo run --example three-spheres; echo
cargo run --example point-on-sphere; echo
cargo run --example kaleidocycle

View file

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

View file

@ -24,7 +24,10 @@ use crate::{
realize_gram,
sphere,
ConfigSubspace,
ConstraintProblem
ConstraintProblem,
DescentHistory,
Realization,
RealizationResult
},
outline::OutlineItem,
specified::SpecifiedValue
@ -547,7 +550,11 @@ pub struct Assembly {
pub tangent: Signal<ConfigSubspace>,
// indexing
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
// realization diagnostics
pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>
}
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 RealizationResult { 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(Realization { config, 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);
}
}

View file

@ -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<String>
}
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::<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

@ -806,6 +806,7 @@ pub fn Display() -> View {
// again
canvas(
ref=display,
id="display",
width="600",
height="600",
tabindex="0",

View file

@ -256,18 +256,18 @@ pub struct DescentHistory {
pub config: Vec<DMatrix<f64>>,
pub scaled_loss: Vec<f64>,
pub neg_grad: Vec<DMatrix<f64>>,
pub min_eigval: Vec<f64>,
pub hess_eigvals: Vec::<DVector<f64>>,
pub base_step: Vec<DMatrix<f64>>,
pub backoff_steps: Vec<i32>
}
impl DescentHistory {
fn new() -> DescentHistory {
pub fn new() -> DescentHistory {
DescentHistory {
config: Vec::<DMatrix<f64>>::new(),
scaled_loss: Vec::<f64>::new(),
neg_grad: Vec::<DMatrix<f64>>::new(),
min_eigval: Vec::<f64>::new(),
hess_eigvals: Vec::<DVector<f64>>::new(),
base_step: Vec::<DMatrix<f64>>::new(),
backoff_steps: Vec::<i32>::new(),
}
@ -393,6 +393,16 @@ fn seek_better_config(
None
}
pub struct Realization {
pub config: DMatrix<f64>,
pub tangent: ConfigSubspace
}
pub struct RealizationResult {
pub result: Result<Realization, String>,
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 +415,7 @@ pub fn realize_gram(
reg_scale: f64,
max_descent_steps: i32,
max_backoff_steps: i32
) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
) -> RealizationResult {
// destructure the problem data
let ConstraintProblem {
gram, guess, frozen
@ -457,11 +467,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 +491,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 RealizationResult {
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 RealizationResult {
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 +537,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(Realization { config: state.config, tangent })
} else {
ConfigSubspace::zero(assembly_dim)
Err("Failed to reach target accuracy".to_string())
};
(state.config, tangent, success, history)
RealizationResult{ result, history }
}
// --- tests ---
@ -539,7 +562,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<f64>, ConfigSubspace, bool, DescentHistory) {
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> RealizationResult {
let mut problem = ConstraintProblem::from_guess(
[
sphere(0.0, 0.0, 0.0, 15.0),
@ -590,7 +613,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<f64>, ConfigSubspace, bool, DescentHistory) {
pub fn realize_kaleidocycle(scaled_tol: f64) -> RealizationResult {
const N_HINGES: usize = 6;
let mut problem = ConstraintProblem::from_guess(
(0..N_HINGES).step_by(2).flat_map(
@ -714,10 +737,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 RealizationResult { 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 +755,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 +782,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 RealizationResult { result, history } = realize_gram(
&problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
let Realization { config, 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 +854,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 RealizationResult { result, history } = realize_kaleidocycle(SCALED_TOL);
let Realization { config, 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 +943,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 RealizationResult { result: result_orig, history: history_orig } = realize_gram(
&problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
let Realization { config: config_orig, tangent: 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 +964,11 @@ mod tests {
guess: guess_tfm,
frozen: problem_orig.frozen
};
let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram(
let RealizationResult { result: result_tfm, history: history_tfm } = realize_gram(
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
let Realization { config: config_tfm, tangent: 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

View file

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

View file

@ -151,7 +151,7 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
.clone()
.into_iter()
.sorted_by_key(|reg| reg.subjects().len())
.collect()
.collect::<Vec<_>>()
);
let details_node = create_node_ref();
view! {
@ -241,7 +241,7 @@ pub fn Outline() -> View {
.clone()
.into_iter()
.sorted_by_key(|elt| elt.id().clone())
.collect()
.collect::<Vec<_>>()
);
view! {