diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59682adf..58003ef5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: matrix: rust: - stable - - 1.78.0 # Minimum supported Rust version + - 1.90.0 # Minimum supported Rust version steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index ea8c4bf7..0c0af5ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/.claude/ diff --git a/Cargo.lock b/Cargo.lock index dfacc118..9cfbcd49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ab_glyph" -version = "0.2.28" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -38,15 +38,15 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.4", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.8.47", ] [[package]] @@ -58,20 +58,14 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "android-activity" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.6.0", + "bitflags 2.11.0", "cc", "cesu8", "jni", @@ -82,7 +76,7 @@ dependencies = [ "ndk-context", "ndk-sys", "num_enum 0.7.3", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -91,12 +85,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" -[[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" @@ -199,11 +187,11 @@ checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] name = "ash" -version = "0.37.3+1.3.251" +version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading 0.7.4", + "libloading", ] [[package]] @@ -224,7 +212,7 @@ dependencies = [ "url", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.6", + "wayland-protocols", "zbus", ] @@ -289,7 +277,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -332,7 +320,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix", + "rustix 0.38.44", "tracing", ] @@ -344,7 +332,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -359,7 +347,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -379,7 +367,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -420,24 +408,24 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -447,9 +435,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block" @@ -457,32 +445,13 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" -[[package]] -name = "block-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" -dependencies = [ - "objc-sys", -] - -[[package]] -name = "block2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" -dependencies = [ - "block-sys", - "objc2 0.4.1", -] - [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2 0.5.2", + "objc2", ] [[package]] @@ -506,22 +475,22 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.7.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -538,26 +507,26 @@ checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "calloop" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "log", "polling", - "rustix", + "rustix 0.38.44", "slab", - "thiserror", + "thiserror 1.0.64", ] [[package]] name = "calloop-wayland-source" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-client", ] @@ -573,6 +542,21 @@ dependencies = [ "wasm-bindgen-cli-support", ] +[[package]] +name = "catppuccin" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be1bbab195d00e745c1788bed24ad8c30678c735d3d9e5bd0f87f9789433971" +dependencies = [ + "itertools 0.14.0", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.117", +] + [[package]] name = "cc" version = "1.1.21" @@ -596,12 +580,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -619,19 +597,29 @@ dependencies = [ "rand 0.6.5", ] +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.0", +] + [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -665,7 +653,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -691,10 +679,11 @@ checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] name = "codespan-reporting" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ + "serde", "termcolor", "unicode-width", ] @@ -711,37 +700,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" -[[package]] -name = "com" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" -dependencies = [ - "com_macros", -] - -[[package]] -name = "com_macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" -dependencies = [ - "com_macros_support", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "com_macros_support" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "combine" version = "4.6.7" @@ -815,7 +773,7 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types", + "core-graphics-types 0.1.3", "foreign-types", "libc", ] @@ -831,6 +789,26 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -846,6 +824,25 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f64009896348fc5af4222e9cf7d7d82a95a256c634ebcf61c53e4ea461422242" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -862,21 +859,16 @@ dependencies = [ ] [[package]] -name = "cursor-icon" -version = "1.1.0" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] -name = "d3d12" -version = "0.19.0" +name = "cursor-icon" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" -dependencies = [ - "bitflags 2.6.0", - "libloading 0.8.5", - "winapi", -] +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" [[package]] name = "deflate" @@ -906,14 +898,14 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.5", + "libloading", ] [[package]] name = "document-features" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -924,39 +916,53 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "ecolor" -version = "0.26.2" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cfe80b1890e1a8cdbffc6044d6872e814aaf6011835a2a5e2db0e5c5c4ef4e" +checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" dependencies = [ "bytemuck", + "emath", ] [[package]] name = "egui" -version = "0.26.2" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180f595432a5b615fc6b74afef3955249b86cfea72607b40740a4cd60d5297d0" +checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" dependencies = [ "ahash", + "bitflags 2.11.0", + "emath", "epaint", "log", "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", ] [[package]] name = "egui-wgpu" -version = "0.26.2" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f2d75e1e70228e7126f828bac05f9fe0e7ea88e9660c8cebe609bb114c61d4" +checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236" dependencies = [ + "ahash", "bytemuck", "document-features", "egui", "epaint", "log", - "thiserror", + "profiling", + "thiserror 2.0.18", "type-map", "web-time", "wgpu", @@ -965,26 +971,21 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.26.2" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4d44f8d89f70d4480545eb2346b76ea88c3022e9f4706cebc799dbe8b004a2" +checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29" dependencies = [ "egui", "log", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "profiling", "raw-window-handle", "web-time", "winit", ] -[[package]] -name = "egui_plot" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803bfcb1ad294dd7f106e26ac9199730d16051496ddb66b10fdb6529eb43df58" -dependencies = [ - "egui", -] - [[package]] name = "either" version = "1.13.0" @@ -993,9 +994,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.26.2" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6916301ecf80448f786cdf3eb51d9dbdd831538732229d49119e2d4312eaaf09" +checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" dependencies = [ "bytemuck", ] @@ -1036,7 +1037,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -1064,20 +1065,28 @@ dependencies = [ [[package]] name = "epaint" -version = "0.26.2" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b9fdf617dd7f58b0c8e6e9e4a1281f730cde0831d40547da446b2bb76a47af" +checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" dependencies = [ "ab_glyph", "ahash", "bytemuck", "ecolor", "emath", + "epaint_default_fonts", "log", "nohash-hasher", "parking_lot", + "profiling", ] +[[package]] +name = "epaint_default_fonts" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" + [[package]] name = "equivalent" version = "1.0.1" @@ -1094,6 +1103,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -1117,9 +1135,9 @@ dependencies = [ [[package]] name = "fallible-iterator" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fastrand" @@ -1137,6 +1155,24 @@ dependencies = [ "miniz_oxide 0.8.0", ] +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1155,7 +1191,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -1248,7 +1284,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -1298,20 +1334,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" -version = "0.26.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", - "indexmap 1.9.3", + "indexmap", "stable_deref_trait", ] @@ -1328,19 +1390,19 @@ dependencies = [ [[package]] name = "glam" -version = "0.24.2" +version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" dependencies = [ "bytemuck", - "serde", + "serde_core", ] [[package]] name = "glow" -version = "0.13.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" dependencies = [ "js-sys", "slotmap", @@ -1387,9 +1449,9 @@ dependencies = [ [[package]] name = "glutin_wgl_sys" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" dependencies = [ "gl_generator", ] @@ -1400,7 +1462,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "gpu-alloc-types", ] @@ -1410,40 +1472,51 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", ] [[package]] name = "gpu-allocator" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", - "thiserror", - "winapi", + "thiserror 1.0.64", "windows", ] [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "gpu-descriptor-types", - "hashbrown 0.14.5", + "hashbrown 0.15.5", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "half" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "bitflags 2.6.0", + "cfg-if", + "crunchy", + "num-traits", + "zerocopy 0.8.47", ] [[package]] @@ -1457,33 +1530,32 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "ahash", - "allocator-api2", + "foldhash 0.1.5", + "serde", ] [[package]] -name = "hassle-rs" -version = "0.11.0" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "bitflags 2.6.0", - "com", - "libc", - "libloading 0.8.5", - "thiserror", - "widestring", - "winapi", + "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -1500,15 +1572,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.1" @@ -1567,7 +1630,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -1579,22 +1642,14 @@ dependencies = [ "cc", ] -[[package]] -name = "icrate" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" -dependencies = [ - "block2 0.3.0", - "dispatch", - "objc2 0.4.1", -] - [[package]] name = "id-arena" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +dependencies = [ + "rayon", +] [[package]] name = "idna" @@ -1624,22 +1679,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg 1.3.0", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.5.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1669,6 +1716,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itertools-num" version = "0.1.3" @@ -1695,7 +1751,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.64", "walkdir", "windows-sys 0.45.0", ] @@ -1723,10 +1779,11 @@ checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1737,7 +1794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.5", + "libloading", "pkg-config", ] @@ -1766,20 +1823,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] -name = "libc" -version = "0.2.170" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "libloading" -version = "0.7.4" +name = "libc" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -1791,13 +1844,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "libc", "redox_syscall 0.4.1", ] @@ -1808,27 +1867,84 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litrs" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg 1.3.0", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9815fac08e6fd96733a11dce4f9d15a3f338e96a2e2311ee21e1b738efc2bc0f" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] [[package]] name = "malloc_buf" @@ -1875,13 +1991,13 @@ dependencies = [ [[package]] name = "metal" -version = "0.27.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types", "log", "objc", @@ -1918,41 +2034,28 @@ dependencies = [ [[package]] name = "naga" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ceaaa4eedaece7e4ec08c55c640ba03dbb73fb812a6570a59bcf1930d0f70e" -dependencies = [ - "bit-set", - "bitflags 2.6.0", - "codespan-reporting", - "hexf-parse", - "indexmap 1.9.3", - "log", - "num-traits", - "rustc-hash", - "termcolor", - "thiserror", - "unicode-xid", -] - -[[package]] -name = "naga" -version = "0.19.2" +version = "27.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" dependencies = [ + "arrayvec", "bit-set", - "bitflags 2.6.0", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", "codespan-reporting", + "half", + "hashbrown 0.16.1", "hexf-parse", - "indexmap 2.5.0", + "indexmap", + "libm", "log", "num-traits", - "rustc-hash", + "once_cell", + "rustc-hash 1.1.0", "spirv", - "termcolor", - "thiserror", - "unicode-xid", + "thiserror 2.0.18", + "unicode-ident", ] [[package]] @@ -1984,17 +2087,17 @@ dependencies = [ [[package]] name = "ndk" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys", "num_enum 0.7.3", "raw-window-handle", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -2005,9 +2108,9 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" -version = "0.5.0+25.2.9519653" +version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ "jni-sys", ] @@ -2028,7 +2131,6 @@ dependencies = [ "chrono", "crude-profiler", "egui", - "egui_plot", "futures", "glam", "hecs", @@ -2063,9 +2165,9 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] @@ -2168,6 +2270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg 1.3.0", + "libm", ] [[package]] @@ -2209,14 +2312,14 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] name = "numpy" -version = "0.21.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec170733ca37175f5d75a5bea5911d6ff45d2cd52849ce98b685394e4f2f37f4" +checksum = "7aac2e6a6e4468ffa092ad43c39b81c79196c2bb773b8db4085f695efe3bba17" dependencies = [ "libc", "nalgebra", @@ -2225,7 +2328,8 @@ dependencies = [ "num-integer", "num-traits", "pyo3", - "rustc-hash", + "pyo3-build-config", + "rustc-hash 2.1.1", ] [[package]] @@ -2235,7 +2339,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -2244,16 +2347,6 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" -[[package]] -name = "objc2" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" -dependencies = [ - "objc-sys", - "objc2-encode 3.0.0", -] - [[package]] name = "objc2" version = "0.5.2" @@ -2261,7 +2354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", - "objc2-encode 4.1.0", + "objc2-encode", ] [[package]] @@ -2270,25 +2363,49 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.6.0", - "block2 0.5.1", + "bitflags 2.11.0", + "block2", "libc", - "objc2 0.5.2", + "objc2", "objc2-core-data", "objc2-core-image", "objc2-foundation", "objc2-quartz-core", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.6.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-foundation", ] @@ -2298,17 +2415,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", + "block2", + "objc2", "objc2-foundation", "objc2-metal", ] [[package]] -name = "objc2-encode" -version = "3.0.0" +name = "objc2-core-location" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] [[package]] name = "objc2-encode" @@ -2322,11 +2445,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", - "block2 0.5.1", + "bitflags 2.11.0", + "block2", "dispatch", "libc", - "objc2 0.5.2", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] @@ -2335,9 +2470,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.6.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-foundation", ] @@ -2347,27 +2482,73 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-foundation", "objc2-metal", ] [[package]] -name = "objc_exception" -version = "0.1.2" +name = "objc2-symbols" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "cc", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "orbclient" @@ -2378,6 +2559,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2390,9 +2580,9 @@ dependencies = [ [[package]] name = "owned_ttf_parser" -version = "0.24.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ "ttf-parser", ] @@ -2423,9 +2613,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2433,15 +2623,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.4", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -2462,6 +2652,26 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2513,7 +2723,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -2536,6 +2746,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.0.10" @@ -2555,7 +2774,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -2564,6 +2783,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2585,30 +2814,29 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" [[package]] name = "pyo3" -version = "0.21.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" dependencies = [ - "cfg-if", "indoc", "libc", "memoffset", - "parking_lot", + "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", @@ -2618,19 +2846,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.21.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.21.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" dependencies = [ "libc", "pyo3-build-config", @@ -2638,47 +2865,59 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.21.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] name = "pyo3-macros-backend" -version = "0.21.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] name = "quick-xml" -version = "0.37.2" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.6.5" @@ -2709,6 +2948,27 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.1.1" @@ -2729,6 +2989,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -2750,9 +3020,24 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_hc" version = "0.1.0" @@ -2834,21 +3119,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] -name = "rdrand" -version = "0.4.0" +name = "rayon" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ - "rand_core 0.3.1", + "either", + "rayon-core", ] [[package]] -name = "redox_syscall" -version = "0.3.5" +name = "rayon-core" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "bitflags 1.3.2", + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] @@ -2866,7 +3162,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", ] [[package]] @@ -2911,12 +3207,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f" dependencies = [ "ashpd", - "block2 0.5.1", + "block2", "core-foundation 0.10.0", "core-foundation-sys", "js-sys", "log", - "objc2 0.5.2", + "objc2", "objc2-app-kit", "objc2-foundation", "pollster 0.4.0", @@ -2947,6 +3243,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2962,10 +3264,23 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.59.0", ] @@ -3004,9 +3319,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sctk-adwaita" -version = "0.8.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b31447ca297092c5a9916fc3b955203157b37c19ca8edde4f52e9843e602c7" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" dependencies = [ "ab_glyph", "log", @@ -3023,22 +3338,32 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -3061,7 +3386,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -3111,30 +3436,30 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smithay-client-toolkit" -version = "0.18.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "calloop", "calloop-wayland-source", "cursor-icon", "libc", "log", "memmap2", - "rustix", - "thiserror", + "rustix 0.38.44", + "thiserror 1.0.64", "wayland-backend", "wayland-client", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols 0.31.2", + "wayland-protocols", "wayland-protocols-wlr", "wayland-scanner", "xkeysym", @@ -3164,7 +3489,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", ] [[package]] @@ -3210,7 +3535,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -3226,9 +3551,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3237,9 +3562,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" @@ -3250,7 +3575,7 @@ dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix", + "rustix 0.38.44", "windows-sys 0.59.0", ] @@ -3269,7 +3594,16 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.64", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -3280,7 +3614,18 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -3335,7 +3680,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.5.0", + "indexmap", "toml_datetime", "winnow 0.5.40", ] @@ -3346,7 +3691,7 @@ version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ - "indexmap 2.5.0", + "indexmap", "toml_datetime", "winnow 0.6.18", ] @@ -3370,7 +3715,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", ] [[package]] @@ -3384,17 +3729,17 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.24.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" [[package]] name = "type-map" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -3485,27 +3830,16 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom", - "rand 0.8.5", - "uuid-macro-internal", + "getrandom 0.4.2", + "js-sys", + "rand 0.10.0", "wasm-bindgen", ] -[[package]] -name = "uuid-macro-internal" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "version_check" version = "0.9.5" @@ -3515,9 +3849,10 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "visula" version = "0.1.0" -source = "git+https://github.com/dragly/visula?rev=d964607cd7cfbf71172314baab1832f6e1b9f523#d964607cd7cfbf71172314baab1832f6e1b9f523" +source = "git+https://github.com/dragly/visula?rev=3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b#3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b" dependencies = [ "bytemuck", + "catppuccin", "cgmath", "chrono", "console_error_panic_hook", @@ -3528,14 +3863,16 @@ dependencies = [ "egui-winit", "env_logger", "futures", + "getrandom 0.3.4", "glam", "gltf", "hecs", - "itertools", + "itertools 0.10.5", "itertools-num", "js-sys", "log", - "naga 0.13.0", + "lyon", + "naga", "ndarray", "num", "numpy", @@ -3543,16 +3880,19 @@ dependencies = [ "pollster 0.3.0", "proc-macro2", "pyo3", + "pyo3-build-config", "quote", - "rand 0.8.5", + "rand 0.9.2", "strum", "syn 1.0.109", + "ttf-parser", "uuid", "visula_core", "visula_derive", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "web-time", "wgpu", "winit", ] @@ -3560,13 +3900,13 @@ dependencies = [ [[package]] name = "visula_core" version = "0.1.0" -source = "git+https://github.com/dragly/visula?rev=d964607cd7cfbf71172314baab1832f6e1b9f523#d964607cd7cfbf71172314baab1832f6e1b9f523" +source = "git+https://github.com/dragly/visula?rev=3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b#3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b" dependencies = [ "bytemuck", "glam", - "itertools", + "itertools 0.10.5", "log", - "naga 0.13.0", + "naga", "uuid", "wgpu", ] @@ -3574,7 +3914,7 @@ dependencies = [ [[package]] name = "visula_derive" version = "0.1.0" -source = "git+https://github.com/dragly/visula?rev=d964607cd7cfbf71172314baab1832f6e1b9f523#d964607cd7cfbf71172314baab1832f6e1b9f523" +source = "git+https://github.com/dragly/visula?rev=3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b#3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -3594,30 +3934,31 @@ dependencies = [ [[package]] name = "walrus" -version = "0.20.3" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c03529cd0c4400a2449f640d2f27cd1b48c3065226d15e26d98e4429ab0adb7" +checksum = "643cc295c2bf4c34d36c2bbaddee48c56a15de3a35e7021b95ceb6a936a493ac" dependencies = [ "anyhow", "gimli", "id-arena", "leb128", "log", + "rayon", "walrus-macro", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", ] [[package]] name = "walrus-macro" -version = "0.19.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7" +checksum = "cef8d704ff46ad931a2cd1f7a504fe43ffb8e968d931e179ff18d0dff4949bd5" dependencies = [ - "heck 0.3.3", + "heck 0.5.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -3627,67 +3968,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn 2.0.77", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-cli-support" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca821da8c1ae6c87c5e94493939a206daa8587caff227c6032e0061a3d80817f" +checksum = "a468565fe14ad40511a77eb46b743d88c7a8dc6e2035a670abb4555a32d9a3e7" dependencies = [ "anyhow", - "base64 0.21.7", + "base64 0.22.1", + "leb128", "log", "rustc-demangle", + "serde", "serde_json", - "tempfile", - "unicode-ident", "walrus", - "wasm-bindgen-externref-xform", - "wasm-bindgen-multi-value-xform", "wasm-bindgen-shared", - "wasm-bindgen-threads-xform", - "wasm-bindgen-wasm-conventions", - "wasm-bindgen-wasm-interpreter", -] - -[[package]] -name = "wasm-bindgen-externref-xform" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102582726b35a30d53157fbf8de3d0f0fed4c40c0c7951d69a034e9ef01da725" -dependencies = [ - "anyhow", - "walrus", + "wasmparser 0.240.0", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -3697,9 +4030,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3707,90 +4040,105 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.77", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] -name = "wasm-bindgen-multi-value-xform" -version = "0.2.92" +name = "wasm-bindgen-shared" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3498e4799f43523d780ceff498f04d882a8dbc9719c28020034822e5952f32a4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ - "anyhow", - "walrus", + "unicode-ident", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] [[package]] -name = "wasm-bindgen-threads-xform" -version = "0.2.92" +name = "wasm-encoder" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5add359b7f7d09a55299a9d29be54414264f2b8cf84f8c8fda5be9269b5dd9" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" dependencies = [ - "anyhow", - "walrus", - "wasm-bindgen-wasm-conventions", + "leb128fmt", + "wasmparser 0.245.1", ] [[package]] -name = "wasm-bindgen-wasm-conventions" -version = "0.2.92" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c04e3607b810e76768260db3a5f2e8beb477cb089ef8726da85c8eb9bd3b575" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "walrus", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] -name = "wasm-bindgen-wasm-interpreter" -version = "0.2.92" +name = "wasmparser" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea966593c8243a33eb4d643254eb97a69de04e89462f46cf6b4f506aae89b3a" +checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" dependencies = [ - "anyhow", - "log", - "walrus", - "wasm-bindgen-wasm-conventions", + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", ] [[package]] -name = "wasm-encoder" -version = "0.29.0" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c41dbd92eaebf3612a39be316540b8377c871cb9bde6b064af962984912881" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "leb128", + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] name = "wasmparser" -version = "0.80.2" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449167e2832691a1bff24cde28d2804e90e09586a448c8e76984792c44334a6b" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", +] [[package]] name = "wayland-backend" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 1.1.4", "scoped-tls", "smallvec", "wayland-sys", @@ -3798,12 +4146,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.8" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ - "bitflags 2.6.0", - "rustix", + "bitflags 2.11.0", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] @@ -3814,7 +4162,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "cursor-icon", "wayland-backend", ] @@ -3825,30 +4173,18 @@ version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a94697e66e76c85923b0d28a0c251e8f0666f58fc47d316c0f4da6da75d37cb" dependencies = [ - "rustix", + "rustix 0.38.44", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" -dependencies = [ - "bitflags 2.6.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.6" +version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -3856,35 +4192,35 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.2.0" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", - "wayland-protocols 0.31.2", + "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-protocols-wlr" -version = "0.2.0" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", - "wayland-protocols 0.31.2", + "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" dependencies = [ "proc-macro2", "quick-xml", @@ -3893,9 +4229,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ "dlib", "log", @@ -3905,9 +4241,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3915,9 +4251,9 @@ dependencies = [ [[package]] name = "web-time" -version = "0.2.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -3925,17 +4261,21 @@ dependencies = [ [[package]] name = "wgpu" -version = "0.19.4" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", + "bitflags 2.11.0", "cfg-if", - "cfg_aliases 0.1.1", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", "js-sys", "log", - "naga 0.19.2", + "naga", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "smallvec", @@ -3950,92 +4290,136 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.19.4" +version = "27.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" dependencies = [ "arrayvec", + "bit-set", "bit-vec", - "bitflags 2.6.0", - "cfg_aliases 0.1.1", - "codespan-reporting", - "indexmap 2.5.0", + "bitflags 2.11.0", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", "log", - "naga 0.19.2", + "naga", "once_cell", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", - "thiserror", - "web-sys", + "thiserror 2.0.18", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-types", ] +[[package]] +name = "wgpu-core-deps-apple" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1027dcf3b027a877e44819df7ceb0e2e98578830f8cd34cd6c3c7c2a7a50b7" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] + [[package]] name = "wgpu-hal" -version = "0.19.5" +version = "27.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.6.0", + "bitflags 2.11.0", "block", - "cfg_aliases 0.1.1", - "core-graphics-types", - "d3d12", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hassle-rs", + "hashbrown 0.16.1", "js-sys", "khronos-egl", "libc", - "libloading 0.8.5", + "libloading", "log", "metal", - "naga 0.19.2", + "naga", "ndk-sys", "objc", "once_cell", + "ordered-float", "parking_lot", + "portable-atomic", + "portable-atomic-util", "profiling", "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", "smallvec", - "thiserror", + "thiserror 2.0.18", "wasm-bindgen", "web-sys", "wgpu-types", - "winapi", + "windows", + "windows-core 0.58.0", ] [[package]] name = "wgpu-types" -version = "0.19.2" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", + "bytemuck", "js-sys", + "log", + "thiserror 2.0.18", "web-sys", ] -[[package]] -name = "widestring" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" - [[package]] name = "winapi" version = "0.3.9" @@ -4069,11 +4453,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.52.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", "windows-targets 0.52.6", ] @@ -4087,21 +4471,72 @@ dependencies = [ ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-core" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-targets 0.42.2", + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.42.2", ] [[package]] @@ -4302,47 +4737,51 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.29.15" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.6.0", + "bitflags 2.11.0", + "block2", "bytemuck", "calloop", - "cfg_aliases 0.1.1", + "cfg_aliases", + "concurrent-queue", "core-foundation 0.9.4", "core-graphics", "cursor-icon", - "icrate", + "dpi", "js-sys", "libc", - "log", "memmap2", "ndk", - "ndk-sys", - "objc2 0.4.1", - "once_cell", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", "orbclient", "percent-encoding", + "pin-project", "raw-window-handle", - "redox_syscall 0.3.5", - "rustix", + "redox_syscall 0.4.1", + "rustix 0.38.44", "sctk-adwaita", "smithay-client-toolkit", "smol_str", + "tracing", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", "wayland-backend", "wayland-client", - "wayland-protocols 0.31.2", + "wayland-protocols", "wayland-protocols-plasma", "web-sys", "web-time", - "windows-sys 0.48.0", + "windows-sys 0.52.0", "x11-dl", "x11rb", "xkbcommon-dl", @@ -4375,6 +4814,94 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -4395,9 +4922,9 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading 0.8.5", + "libloading", "once_cell", - "rustix", + "rustix 0.38.44", "x11rb-protocol", ] @@ -4429,7 +4956,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "dlib", "log", "once_cell", @@ -4493,7 +5020,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -4518,7 +5045,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive 0.8.47", ] [[package]] @@ -4529,7 +5065,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -4557,7 +5104,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.117", "zvariant_utils", ] @@ -4571,6 +5118,6 @@ dependencies = [ "quote", "serde", "static_assertions", - "syn 2.0.77", + "syn 2.0.117", "winnow 0.7.3", ] diff --git a/neuronify-core/Cargo.toml b/neuronify-core/Cargo.toml index 38b36518..ef391735 100644 --- a/neuronify-core/Cargo.toml +++ b/neuronify-core/Cargo.toml @@ -7,19 +7,18 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -visula = { git = "https://github.com/dragly/visula", rev = "d964607cd7cfbf71172314baab1832f6e1b9f523" } -visula_derive = { git = "https://github.com/dragly/visula", rev = "d964607cd7cfbf71172314baab1832f6e1b9f523" } -visula_core = { git = "https://github.com/dragly/visula", rev = "d964607cd7cfbf71172314baab1832f6e1b9f523" } -wgpu = { version = "0.19", features = ["webgl"] } -glam = { version = "0.24", features = ["bytemuck", "serde"] } -bytemuck = { version = "1.4", features = ["derive"] } +visula = { git = "https://github.com/dragly/visula", rev = "3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b" } +visula_derive = { git = "https://github.com/dragly/visula", rev = "3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b" } +visula_core = { git = "https://github.com/dragly/visula", rev = "3f8486cdb293faf9b95dfc29f69449f5ba5c6f5b" } +wgpu = { version = "27", features = ["webgl"] } +glam = { version = "0.30", features = ["bytemuck", "serde"] } +bytemuck = { version = "1.24", features = ["derive"] } log = "0.4" -egui = "0.26" -egui_plot = "0.26" +egui = "0.33" pollster = "0.3.0" futures = "0.3" wasm-bindgen-futures = "0.4" -wasm-bindgen = "=0.2.92" +wasm-bindgen = "0.2.104" cgmath = "0.17.0" js-sys = "0.3" ndarray = "0.15.3" @@ -28,7 +27,7 @@ syn = { version = "1.0.80", features = ["parsing"] } quote = "1.0.10" proc-macro2 = "1.0.29" crude-profiler = "0.1.7" -hecs = { version = "0.10.3", features = [ +hecs = { version = "0.10.5", features = [ "column-serialize", "serde", "row-serialize", @@ -42,4 +41,4 @@ postcard = { version = "1.0.8", features = ["use-std"] } web-sys = { version = "0.3.69", features = ["Request", "Response", "RequestInit", "Headers"] } chrono = { version = "0.4.38", features = ["serde"] } rfd = "0.15.2" -winit = "0.29" +winit = "0.30" diff --git a/neuronify-core/examples/adaptation.nfy b/neuronify-core/examples/adaptation.nfy new file mode 100644 index 00000000..5093caa2 --- /dev/null +++ b/neuronify-core/examples/adaptation.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/ImmediateFireSynapse.qml","from":1,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":3,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":3,"nodes":[{"filename":"neurons/AdaptationNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07559084389820746,"adaptation":1e-8,"timeConstant":0.5},"inhibitory":false,"label":"Adaptive","x":576,"y":512}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":238.73142025657265,"y":493.5002436540507}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.000303,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07559084389820746,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Leaky","x":416,"y":512}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":704,"y":448,"height":192,"maximumValue":50,"minimumValue":-100,"width":256}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":160,"y":640,"height":160,"text":"Touch the sensor to make the leaky neuron fire.","width":224}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":576,"y":704,"height":192,"text":"Observe how the adaptive neuron becomes harder to excite for every time it fires. After a recovery period, it does however return back to normal.","width":288}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":666.5373883928576,"width":1091.4062500000007,"x":33.175780012669755,"y":321.5314732381097}}} diff --git a/neuronify-core/examples/burst.nfy b/neuronify-core/examples/burst.nfy new file mode 100644 index 00000000..50a1d7cd --- /dev/null +++ b/neuronify-core/examples/burst.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/ImmediateFireSynapse.qml","from":1,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":2,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":3,"nodes":[{"filename":"neurons/BurstNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06965919995225374},"inhibitory":false,"label":"","x":416,"y":416}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":233.52329545454518,"y":398.91559248205203}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":576,"y":320,"height":256,"maximumValue":50,"minimumValue":-100,"width":320}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":192,"y":544,"height":128,"text":"Touch to make the neuron fire","width":192}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":608,"y":608,"height":160,"text":"The bursting neuron fires continously for some time before it stops.","width":256}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":520.2243031358886,"width":851.8292682926831,"x":152.50432240523696,"y":279.9379977190913}}} \ No newline at end of file diff --git a/neuronify-core/examples/disinhibition.nfy b/neuronify-core/examples/disinhibition.nfy new file mode 100644 index 00000000..b7e23a3a --- /dev/null +++ b/neuronify-core/examples/disinhibition.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":2,"savedProperties":{"filename":"Edge.qml"},"to":3},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00004768474077305635,"linear":0,"maximumCurrent":4e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":4},{"filename":"edges/CurrentSynapse.qml","from":5,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.024894280619189486,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":4},{"filename":"edges/MeterEdge.qml","from":0,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":1,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":4},{"filename":"Edge.qml","from":2,"savedProperties":{"filename":"Edge.qml"},"to":5},{"filename":"edges/CurrentSynapse.qml","from":9,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.332e-320,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":5},{"filename":"edges/ImmediateFireSynapse.qml","from":6,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":9}],"fileFormatVersion":4,"nodes":[{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":832,"y":160,"height":180,"maximumValue":50,"minimumValue":-100,"width":320}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":833,"y":352,"height":181,"maximumValue":50,"minimumValue":-100,"width":317}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":353,"y":224}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00030099999999999994,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07816414183709385,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Input","x":512,"y":224}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00039999999999999996,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.089407415613173,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Output","x":512,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.00023899999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06370544826122049,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":352,"y":384}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":29.455074198175055,"y":362.4594716921337}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-31,"y":2,"height":288,"text":"Try out disinhibition\nActivate the inhibitory neuron and release the brake on the excitatory output neuron. What happens to the output?","width":352}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-19,"y":556,"height":243,"text":"Disinhibition is an important principle found in many neural circuits. \n\nRead more about disinhibition as a circuit mechanism for associative learning and memory (Letzkus, Wolff and Luhti, Cell, 2015)","width":390}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000014,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":192,"y":384}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":884.2059341767565,"width":1567.0170689934405,"x":-134.81310344605265,"y":-17.658527468946808}}} \ No newline at end of file diff --git a/neuronify-core/examples/empty.nfy b/neuronify-core/examples/empty.nfy new file mode 100644 index 00000000..65881b0f --- /dev/null +++ b/neuronify-core/examples/empty.nfy @@ -0,0 +1 @@ +{"nodes": [], "edges": []} diff --git a/neuronify-core/examples/fhn_chain.rs b/neuronify-core/examples/fhn_chain.rs new file mode 100644 index 00000000..2b8c2c14 --- /dev/null +++ b/neuronify-core/examples/fhn_chain.rs @@ -0,0 +1,151 @@ +const NUM_COMPARTMENTS: usize = 10; +const DT: f64 = 0.01; +const STEPS: usize = 1500; +const PRINT_EVERY: usize = 5; + +const TAU: f64 = 60.0; +const A: f64 = 0.7; +const B: f64 = 0.8; +const EPSILON: f64 = 0.08; + +const COUPLING: f64 = 24.0; + +const FIRE_V: f64 = 1.0; + +const FIRE_EVERY: usize = 200; + +const V_SCALE: f64 = 50.0; +const V_OFFSET: f64 = 50.0; + +#[derive(Clone)] +struct Compartment { + v: f64, + w: f64, +} + +impl Compartment { + fn new() -> Self { + Self { v: -1.2, w: -0.625 } + } + + fn fire(&mut self) { + self.v = FIRE_V; + } + + fn display_voltage(&self) -> f64 { + self.v * V_SCALE + V_OFFSET + } +} + +fn fhn_step(comp: &mut Compartment) { + let v = comp.v; + let w = comp.w; + + let dv = TAU * (v - v * v * v / 3.0 - w); + let dw = TAU * EPSILON * (v + A - B * w); + + comp.v += dv * DT; + comp.w += dw * DT; +} + +fn coupling_step(compartments: &mut [Compartment]) { + let old: Vec = compartments.iter().map(|c| c.v).collect(); + for i in 0..compartments.len() { + if i > 0 { + let diff = old[i - 1] - old[i]; + compartments[i].v += COUPLING * diff * DT; + compartments[i - 1].v -= COUPLING * diff * DT; + } + } +} + +fn print_state(step: usize, compartments: &[Compartment]) { + print!("step {:>4} | ", step); + for comp in compartments { + let dv = comp.display_voltage(); + let ch = if dv > 80.0 { + '#' + } else if dv > 50.0 { + '*' + } else if dv > 20.0 { + '+' + } else if dv > 5.0 { + '.' + } else { + ' ' + }; + print!("{:>7.1} {} ", dv, ch); + } + println!(); +} + +fn main() { + println!("FHN Chain Simulation — {} compartments", NUM_COMPARTMENTS); + println!( + "Parameters: a={}, b={}, eps={}, coupling={}, fire_v={}", + A, B, EPSILON, COUPLING, FIRE_V + ); + println!(); + + print!("{:>14}", ""); + for i in 0..NUM_COMPARTMENTS { + print!(" C{:<6}", i); + } + println!(); + println!("{}", "-".repeat(14 + NUM_COMPARTMENTS * 10)); + + let mut compartments: Vec = + (0..NUM_COMPARTMENTS).map(|_| Compartment::new()).collect(); + + compartments[0].fire(); + + for step in 0..STEPS { + if FIRE_EVERY > 0 && step > 0 && step % FIRE_EVERY == 0 { + compartments[0].fire(); + println!( + "--- RE-FIRE at step {} (t = {:.2}) ---", + step, + step as f64 * DT + ); + } + + for comp in compartments.iter_mut() { + fhn_step(comp); + } + coupling_step(&mut compartments); + + if step % PRINT_EVERY == 0 { + print_state(step, &compartments); + } + } + + println!(); + println!("Peak detection (re-running):"); + let mut compartments: Vec = + (0..NUM_COMPARTMENTS).map(|_| Compartment::new()).collect(); + compartments[0].fire(); + let mut peaks = [(f64::MIN, 0_usize); NUM_COMPARTMENTS]; + + for step in 0..STEPS { + for comp in compartments.iter_mut() { + fhn_step(comp); + } + coupling_step(&mut compartments); + + for (i, comp) in compartments.iter().enumerate() { + if comp.display_voltage() > peaks[i].0 { + peaks[i] = (comp.display_voltage(), step); + } + } + } + + for (i, (peak_v, peak_step)) in peaks.iter().enumerate() { + println!( + " C{}: peak = {:.1} at step {} (t = {:.2})", + i, + peak_v, + peak_step, + *peak_step as f64 * DT + ); + } +} diff --git a/neuronify-core/examples/frPlot.nfy b/neuronify-core/examples/frPlot.nfy new file mode 100644 index 00000000..de279aae --- /dev/null +++ b/neuronify-core/examples/frPlot.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.01490512538247481,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07561178602532832,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":896,"y":-384}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":640,"y":-704,"height":224,"text":"The firing rate plot shows the firing rate measured in spikes per second of one or more neurons. When connected to more than one neuron, the mean population firing rate is shown.","width":416}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":1440,"y":-480,"height":224,"text":"The rate is calculated on-the-fly using a Gaussian window[1], where the window width can be adjusted by the user. \n\n[1] Theoretical Neuroscience, Dayan & Abbot, 2005\n","width":384}},{"filename":"generators/RegularSpikeGenerator.qml","savedProperties":{"engine":{"rate":100},"inhibitory":false,"label":"","x":704,"y":-384}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":704,"y":-192,"height":192,"text":"Connect the firing rate plot to the second neuron which receives no input. What happens? ","width":320}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07001948657567676,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":896,"y":-288}},{"filename":"meters/RatePlot.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1088,"y":-480,"height":240,"maximumValue":100,"minimumValue":0,"showLegend":true,"temporalResolution":0.2,"width":320,"windowDuration":0.3}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":1123.9938490906202,"width":1772.9664881488948,"x":406.57683413999206,"y":-835.6695192367445}}} diff --git a/neuronify-core/examples/generators.nfy b/neuronify-core/examples/generators.nfy new file mode 100644 index 00000000..aae1be24 --- /dev/null +++ b/neuronify-core/examples/generators.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":5,"savedProperties":{"filename":"Edge.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":7,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.01738460461580383,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":3},{"filename":"Edge.qml","from":6,"savedProperties":{"filename":"Edge.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":8,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.32353354497370934,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/MeterEdge.qml","from":4,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":1},{"filename":"edges/MeterEdge.qml","from":4,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":4,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":4,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":2}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.09,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"AC current source","x":896,"y":-352}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.059622759367088676,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Regular Spike generator","x":896,"y":-160}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0799252619770881,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"DC current source","x":896,"y":-448}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.05760477579646065,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Irregular spike generator","x":896,"y":-256}},{"filename":"meters/SpikeDetector.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1152,"y":-448,"height":352,"showLegend":true,"timeRange":0.101,"width":448}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":768,"y":-448}},{"filename":"generators/ACClamp.qml","savedProperties":{"engine":{"amplitude":1e-9,"frequency":34,"time":5.175300000001277},"inhibitory":false,"label":"","x":768,"y":-352}},{"filename":"generators/IrregularSpikeGenerator.qml","savedProperties":{"engine":{"rate":200},"inhibitory":false,"label":"","x":768,"y":-256}},{"filename":"generators/RegularSpikeGenerator.qml","savedProperties":{"engine":{"rate":200},"inhibitory":false,"label":"","x":768,"y":-160}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":588,"width":1017.8823529411765,"x":697.0272457993925,"y":-569.7315455847197}}} diff --git a/neuronify-core/examples/hh_chain.rs b/neuronify-core/examples/hh_chain.rs new file mode 100644 index 00000000..76992958 --- /dev/null +++ b/neuronify-core/examples/hh_chain.rs @@ -0,0 +1,207 @@ +const NUM_COMPARTMENTS: usize = 10; +const CDT: f64 = 0.01; +const STEPS: usize = 400; +const PRINT_EVERY: usize = 5; + +const CAPACITANCE: f64 = 1.0; + +const COUPLING_CAPACITANCE: f64 = 0.05; + +const FIRE_IMPULSE_INITIAL: f64 = 800.0; +const FIRE_IMPULSE_DECAY: f64 = 5.0; + +const FIRE_EVERY: usize = 0; + +const RECOVERY_RATE: f64 = 10.0; + +const G_NA: f64 = 120.0; +const G_K: f64 = 36.0; +const LEAK_CONDUCTANCE: f64 = 1.3; + +const E_NA: f64 = 115.0; +const E_K: f64 = -12.0; +const E_M: f64 = 10.6; + +#[derive(Clone)] +struct Compartment { + voltage: f64, + m: f64, + h: f64, + n: f64, + capacitance: f64, + injected_current: f64, + fire_impulse: f64, +} + +impl Compartment { + fn new() -> Self { + Self { + voltage: 0.0, + m: 0.05, + h: 0.6, + n: 0.32, + capacitance: CAPACITANCE, + injected_current: 0.0, + fire_impulse: 0.0, + } + } +} + +fn fire_compartment(comp: &mut Compartment) { + comp.fire_impulse = FIRE_IMPULSE_INITIAL; + comp.m = 0.05; + comp.h = 0.6; + comp.n = 0.32; + comp.voltage = 0.0; +} + +fn hh_step(comp: &mut Compartment) { + let v = comp.voltage; + + let alpha_m = 0.1 * (25.0 - v) / ((2.5 - 0.1 * v).exp() - 1.0); + let beta_m = 4.0 * (-v / 18.0).exp(); + let dm = CDT * (alpha_m * (1.0 - comp.m) - beta_m * comp.m); + comp.m = (comp.m + dm).clamp(0.0, 1.0); + + let alpha_h = 0.07 * (-v / 20.0).exp(); + let beta_h = 1.0 / ((3.0 - 0.1 * v).exp() + 1.0); + let dh = CDT * (alpha_h * (1.0 - comp.h) - beta_h * comp.h); + comp.h = (comp.h + dh).clamp(0.0, 1.0); + + let alpha_n = 0.01 * (10.0 - v) / ((1.0 - 0.1 * v).exp() - 1.0); + let beta_n = 0.125 * (-v / 80.0).exp(); + let dn = CDT * (alpha_n * (1.0 - comp.n) - beta_n * comp.n); + comp.n = (comp.n + dn).clamp(0.0, 1.0); + + let m3 = comp.m * comp.m * comp.m; + let n4 = comp.n * comp.n * comp.n * comp.n; + let sodium_current = -G_NA * m3 * comp.h * (comp.voltage - E_NA); + let potassium_current = -G_K * n4 * (comp.voltage - E_K); + let leak_current = -LEAK_CONDUCTANCE * (comp.voltage - E_M); + + let current = sodium_current + potassium_current + leak_current + comp.injected_current; + let delta_voltage = current / comp.capacitance; + comp.voltage += delta_voltage * CDT; + + if comp.fire_impulse > 1.0 { + comp.voltage += comp.fire_impulse; + comp.fire_impulse *= (-FIRE_IMPULSE_DECAY * CDT).exp(); + } + + comp.voltage = comp.voltage.clamp(-50.0, 200.0); + comp.injected_current -= 1.0 * comp.injected_current * CDT; + + if comp.voltage < 0.0 { + comp.h += (0.6 - comp.h) * RECOVERY_RATE * CDT; + comp.n += (0.32 - comp.n) * RECOVERY_RATE * CDT; + comp.m += (0.05 - comp.m) * RECOVERY_RATE * CDT; + } +} + +fn coupling_step(compartments: &mut [Compartment]) { + let old: Vec = compartments.to_vec(); + for i in 0..compartments.len() { + if i > 0 { + let voltage_diff = old[i - 1].voltage - old[i].voltage; + let delta = voltage_diff / COUPLING_CAPACITANCE * CDT; + compartments[i].voltage += delta; + compartments[i - 1].voltage -= delta; + } + } +} + +fn print_state(step: usize, compartments: &[Compartment]) { + print!("step {:>4} | ", step); + for comp in compartments { + let v = comp.voltage; + let ch = if v > 80.0 { + '#' + } else if v > 50.0 { + '*' + } else if v > 20.0 { + '+' + } else if v > 5.0 { + '.' + } else { + ' ' + }; + print!("{:>7.1} {} ", v, ch); + } + println!(); +} + +fn main() { + println!("HH Chain Simulation — {} compartments", NUM_COMPARTMENTS); + println!( + "Parameters: C={}, coupling_C={}, fire_impulse={}, decay={}", + CAPACITANCE, COUPLING_CAPACITANCE, FIRE_IMPULSE_INITIAL, FIRE_IMPULSE_DECAY + ); + println!( + "HH: g_na={}, g_k={}, leak={}, E_na={}, E_k={}, E_m={}", + G_NA, G_K, LEAK_CONDUCTANCE, E_NA, E_K, E_M + ); + println!(); + + print!("{:>14}", ""); + for i in 0..NUM_COMPARTMENTS { + print!(" C{:<6}", i); + } + println!(); + println!("{}", "-".repeat(14 + NUM_COMPARTMENTS * 10)); + + let mut compartments: Vec = + (0..NUM_COMPARTMENTS).map(|_| Compartment::new()).collect(); + + fire_compartment(&mut compartments[0]); + + for step in 0..STEPS { + if FIRE_EVERY != 0 && step > 0 && step % FIRE_EVERY == 0 { + fire_compartment(&mut compartments[0]); + println!( + "--- RE-FIRE at step {} (t = {:.2} ms) ---", + step, + step as f64 * CDT + ); + } + + for comp in compartments.iter_mut() { + hh_step(comp); + } + + coupling_step(&mut compartments); + + if step % PRINT_EVERY == 0 { + print_state(step, &compartments); + } + } + + println!(); + println!("Peak detection (re-running):"); + let mut compartments: Vec = + (0..NUM_COMPARTMENTS).map(|_| Compartment::new()).collect(); + compartments[0].fire_impulse = FIRE_IMPULSE_INITIAL; + let mut peaks = [(0.0_f64, 0_usize); NUM_COMPARTMENTS]; + + for step in 0..STEPS { + for comp in compartments.iter_mut() { + hh_step(comp); + } + coupling_step(&mut compartments); + + for (i, comp) in compartments.iter().enumerate() { + if comp.voltage > peaks[i].0 { + peaks[i] = (comp.voltage, step); + } + } + } + + for (i, (peak_v, peak_step)) in peaks.iter().enumerate() { + println!( + " C{}: peak = {:.1} mV at step {} (t = {:.2} ms)", + i, + peak_v, + peak_step, + *peak_step as f64 * CDT + ); + } +} diff --git a/neuronify-core/examples/if_response.nfy b/neuronify-core/examples/if_response.nfy new file mode 100644 index 00000000..c40ca87f --- /dev/null +++ b/neuronify-core/examples/if_response.nfy @@ -0,0 +1,220 @@ +{ + "edges": [ + { + "filename": "Edge.qml", + "from": 3, + "savedProperties": { + "filename": "Edge.qml" + }, + "to": 2 + }, + { + "filename": "Edge.qml", + "from": 4, + "savedProperties": { + "filename": "Edge.qml" + }, + "to": 0 + }, + { + "filename": "Edge.qml", + "from": 8, + "savedProperties": { + "filename": "Edge.qml" + }, + "to": 1 + }, + { + "filename": "edges/MeterEdge.qml", + "from": 5, + "savedProperties": { + "filename": "edges/MeterEdge.qml" + }, + "to": 2 + }, + { + "filename": "edges/MeterEdge.qml", + "from": 7, + "savedProperties": { + "filename": "edges/MeterEdge.qml" + }, + "to": 0 + }, + { + "filename": "edges/MeterEdge.qml", + "from": 6, + "savedProperties": { + "filename": "edges/MeterEdge.qml" + }, + "to": 1 + } + ], + "fileFormatVersion": 4, + "nodes": [ + { + "filename": "neurons/LeakyNeuron.qml", + "savedProperties": { + "engine": { + "capacitance": 9.999999999999999e-10, + "fireOutput": 0.00019999999999999998, + "initialPotential": 0, + "maximumVoltage": 0.06, + "minimumVoltage": -0.09, + "restingPotential": 0, + "threshold": 0.020000000000000004, + "voltage": 0.018579150989776978, + "voltageClamped": true, + "refractoryPeriod": 0, + "resistance": 10000000 + }, + "inhibitory": false, + "label": "", + "x": 480, + "y": 288 + } + }, + { + "filename": "neurons/LeakyNeuron.qml", + "savedProperties": { + "engine": { + "capacitance": 9.999999999999999e-10, + "fireOutput": 0.00019999999999999998, + "initialPotential": 0, + "maximumVoltage": 0.06, + "minimumVoltage": -0.09, + "restingPotential": 0, + "threshold": 0.020000000000000004, + "voltage": 0.014576124308745613, + "voltageClamped": true, + "refractoryPeriod": 0, + "resistance": 10000000 + }, + "inhibitory": false, + "label": "", + "x": 480, + "y": 512 + } + }, + { + "filename": "neurons/LeakyNeuron.qml", + "savedProperties": { + "engine": { + "capacitance": 9.999999999999999e-10, + "fireOutput": 0.00019999999999999998, + "initialPotential": 0, + "maximumVoltage": 0.06, + "minimumVoltage": -0.09, + "restingPotential": 0, + "threshold": 0.020000000000000004, + "voltage": 0.017999992150682296, + "voltageClamped": true, + "refractoryPeriod": 0, + "resistance": 10000000 + }, + "inhibitory": false, + "label": "", + "x": 480, + "y": 64 + } + }, + { + "filename": "generators/CurrentClamp.qml", + "savedProperties": { + "engine": { + "currentOutput": 1.8e-09 + }, + "inhibitory": false, + "label": "", + "x": 320, + "y": 64 + } + }, + { + "filename": "generators/CurrentClamp.qml", + "savedProperties": { + "engine": { + "currentOutput": 2.02e-09 + }, + "inhibitory": false, + "label": "", + "x": 320, + "y": 288 + } + }, + { + "filename": "meters/Voltmeter.qml", + "savedProperties": { + "engine": {}, + "inhibitory": false, + "label": "", + "x": 640, + "y": 0, + "height": 199.65449138498548, + "maximumValue": 30.329999999999984, + "minimumValue": 0, + "width": 327.38734648475645 + } + }, + { + "filename": "meters/Voltmeter.qml", + "savedProperties": { + "engine": {}, + "inhibitory": false, + "label": "", + "x": 640, + "y": 448, + "height": 214.48275862068954, + "maximumValue": 30.329999999999984, + "minimumValue": 0, + "width": 335.6425506798937 + } + }, + { + "filename": "meters/Voltmeter.qml", + "savedProperties": { + "engine": {}, + "inhibitory": false, + "label": "", + "x": 640, + "y": 224, + "height": 210.9618396858187, + "maximumValue": 30.329999999999984, + "minimumValue": 0, + "width": 332.7652039681277 + } + }, + { + "filename": "generators/CurrentClamp.qml", + "savedProperties": { + "engine": { + "currentOutput": 2.2e-09 + }, + "inhibitory": false, + "label": "", + "x": 320, + "y": 512 + } + }, + { + "filename": "annotations/Note.qml", + "savedProperties": { + "inhibitory": false, + "label": "", + "x": 1016, + "y": 0, + "height": 670, + "text": "The level of current injection must be high enough to bring the membrane potential to the firing threshold, otherwise the cell will not fire.\n\nAbove the firing threshold, higher level of current injection causes firing at a higher frequency.\n\nNote that the resting potential and firing threshold in this example is artificial and set to 0 mV and +20 mV, respectively. A set of more biologically plausible values would be -65 mV for the resting potential and -50 mV for the firing threshold.\n\nThis example is from chapter 8 (figure 8.5a) in Principles of Computational Modelling in Neuroscience. Sterratt, David. Cambridge: Cambridge UP, 2011.", + "width": 346 + } + } + ], + "workspace": { + "playbackSpeed": 2, + "visibleRectangle": { + "height": 842.7263586812722, + "width": 1493.5056840279644, + "x": 131.87906128971252, + "y": -116.84233435043635 + } + } +} diff --git a/neuronify-core/examples/inhibitory.nfy b/neuronify-core/examples/inhibitory.nfy new file mode 100644 index 00000000..2250d0a6 --- /dev/null +++ b/neuronify-core/examples/inhibitory.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.1240210905955874e-34,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.9931410626154762e-24,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/ImmediateFireSynapse.qml","from":4,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":2},{"filename":"edges/ImmediateFireSynapse.qml","from":5,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":1,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00000199999999999994,"initialPotential":-0.07,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07020723852602385,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"C","x":960,"y":672}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1120,"y":576,"height":252.77176249895882,"maximumValue":50,"minimumValue":-200,"width":383.3115308902113}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.07,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000055341051777,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"A","x":800,"y":576}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.07,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000580794558932,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"B","x":800,"y":768}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":634.6051196667249,"y":556.4807294895742}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":636.4997108704418,"y":744.992554259366}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":576,"y":896,"height":160,"text":"Touch this sensor to fire the inhibitory neuron B.","width":224}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":576,"y":352,"height":160,"text":"Touch this sensor to fire the excitatory neuron A.","width":224}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":1152,"y":864,"height":269,"text":"Observe how the excitatory neuron increases the membrane potential while the inhibitory lowers the membrane potential of neuron C.","width":328}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":808.202158718258,"width":1432.3208304273653,"x":335.297397572757,"y":377.5970619382823}}} diff --git a/neuronify-core/examples/input_summation.nfy b/neuronify-core/examples/input_summation.nfy new file mode 100644 index 00000000..9cd82996 --- /dev/null +++ b/neuronify-core/examples/input_summation.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.332e-320,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":6},{"filename":"edges/MeterEdge.qml","from":5,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":6},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.332e-320,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":6},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.332e-320,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":6},{"filename":"edges/CurrentSynapse.qml","from":0,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.8492008962109037e-8,"linear":0,"maximumCurrent":2e-9,"tau":0.001,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":7},{"filename":"edges/MeterEdge.qml","from":4,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":7},{"filename":"Edge.qml","from":8,"savedProperties":{"filename":"Edge.qml"},"to":0},{"filename":"edges/ImmediateFireSynapse.qml","from":10,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":1},{"filename":"edges/ImmediateFireSynapse.qml","from":11,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":2},{"filename":"edges/ImmediateFireSynapse.qml","from":12,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":3}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0799252619770881,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":256,"y":224}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000014,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":256,"y":416}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000014,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":256,"y":512}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000014,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":256,"y":608}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":576,"y":128,"height":240,"maximumValue":50,"minimumValue":-100,"width":320}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":576,"y":416,"height":240,"maximumValue":50,"minimumValue":-100,"width":320}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.04,"voltage":-0.0700000000000014,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":384,"y":512}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.04,"voltage":-0.06316041814741598,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":384,"y":224}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":96,"y":224}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":96,"y":-64,"height":256,"text":"Input summation:\n\nMost neurons are connected with many neurons and require more than one synaptic input to reach \n threshold and fire. The sum of synaptic input over time determines the firing of most neurons. ","width":384}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":96,"y":396}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":94,"y":493}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":93,"y":589}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-249,"y":378,"height":319,"text":"Spatial summation:\nHow many neurons do you need to activate with the touch receptor to get the output neuron to fire an action potential?\n\nTemporal summation: \nBy repeatedly activating one neuron, summation in time is enough to reach threshold.","width":290}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":1020.293020753993,"width":2030.9606356518163,"x":-430.1774084254163,"y":-64.53770385637047}}} \ No newline at end of file diff --git a/neuronify-core/examples/lateral_inhibition.nfy b/neuronify-core/examples/lateral_inhibition.nfy new file mode 100644 index 00000000..c64f2833 --- /dev/null +++ b/neuronify-core/examples/lateral_inhibition.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":5,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":5.946997335416537e-121,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":10},{"filename":"edges/CurrentSynapse.qml","from":6,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.6404013352845583e-122,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":11},{"filename":"edges/CurrentSynapse.qml","from":7,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":8.373735155067851e-124,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":12},{"filename":"edges/CurrentSynapse.qml","from":8,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.559318762514483e-125,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":13},{"filename":"edges/CurrentSynapse.qml","from":14,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":6.589470731763476e-121,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":18},{"filename":"edges/CurrentSynapse.qml","from":15,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":3.540756285671349e-122,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":18},{"filename":"edges/CurrentSynapse.qml","from":16,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.1990960817798994e-123,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":18},{"filename":"edges/CurrentSynapse.qml","from":17,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":7.514994541250221e-125,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":18},{"filename":"edges/CurrentSynapse.qml","from":10,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.269456006810955e-119,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":14},{"filename":"edges/CurrentSynapse.qml","from":11,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":6.589470731763476e-121,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":15},{"filename":"edges/CurrentSynapse.qml","from":12,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":3.540756285671349e-122,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":16},{"filename":"edges/CurrentSynapse.qml","from":13,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.1990960817798994e-123,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":17},{"filename":"edges/CurrentSynapse.qml","from":9,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.447592341158551e-126,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":17},{"filename":"edges/CurrentSynapse.qml","from":8,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.559318762514483e-125,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":16},{"filename":"edges/CurrentSynapse.qml","from":7,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":8.373735155067851e-124,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":15},{"filename":"edges/CurrentSynapse.qml","from":6,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.6404013352845583e-122,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":14},{"filename":"edges/ImmediateFireSynapse.qml","from":20,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":6},{"filename":"edges/ImmediateFireSynapse.qml","from":21,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":7},{"filename":"edges/ImmediateFireSynapse.qml","from":22,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":8},{"filename":"edges/ImmediateFireSynapse.qml","from":23,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":9},{"filename":"edges/ImmediateFireSynapse.qml","from":19,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":5},{"filename":"edges/MeterEdge.qml","from":0,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":18}],"fileFormatVersion":4,"nodes":[{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":640,"y":800,"height":224,"maximumValue":50,"minimumValue":-100,"width":320}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":64,"y":224,"height":192,"text":"Look at all those touch activators!\n\nTry touching them from left to right or from right to left. \n\nWe won't tell anyone you did.","width":320}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":224,"y":480,"height":192,"text":"The inhibitory neurons inhibit one step forward and to the right in the network. ","width":256}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":1021.7365791600741,"y":512,"height":288,"text":"If you are touching them from left to right, the relay neurons are already inhibited.\n\nHowever, if you do it from right to left, the neuron becomes inhibited after the signal has already passed through.","width":325.34883720930236}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":224,"y":800,"height":223,"text":"The output neuron only fires when you touch them from right to left.\n\nThis type of network could be used in your eyes to detect movement in only one direction.\n\nJust don't touch your eyes.","width":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00039999999999999996,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000001724,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Inputs","x":480,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000001214,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":576,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000911,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":672,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000065,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":768,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00039999999999999996,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000495,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":864,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.0004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000001022,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"Feedforward inhibition","x":544,"y":480}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.0004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000767,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":640,"y":480}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.0004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000611,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":736,"y":480}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.0004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000478,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":832,"y":480}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00014999999999999996,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000009289,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Relay","x":608,"y":576}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00015,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000006638,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":704,"y":576}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00015,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000501,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":800,"y":576}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00015,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000003644,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":896,"y":576}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00039999999999999996,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06999999999999135,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Output","x":768,"y":704}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":440.44944210480054,"y":230.47331976207707}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":547.1802113355698,"y":231.43485822361544}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":653.9109805663393,"y":232.3963966851539}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":755.834057489416,"y":231.4348582236155}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":854.8725190278778,"y":230.47331976207693}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":919.3244226824595,"width":1647.2536583900724,"x":-49.94070909340277,"y":166.37947469291763}}} \ No newline at end of file diff --git a/neuronify-core/examples/lateral_inhibition_1.nfy b/neuronify-core/examples/lateral_inhibition_1.nfy new file mode 100644 index 00000000..219ea3b1 --- /dev/null +++ b/neuronify-core/examples/lateral_inhibition_1.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/MeterEdge.qml","from":7,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00017190531077428933,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00017190531077428933,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":5},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00017190531077428933,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":4},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00017190531077428933,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":8},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00017190531077428933,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":9},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":9},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":8},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":7,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":7,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":5,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.006905413874132118,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":4,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.006905413874132118,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":9},{"filename":"edges/CurrentSynapse.qml","from":14,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.29198902433877266,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":14,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.29198902433877266,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":3},{"filename":"edges/CurrentSynapse.qml","from":14,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.29198902433877266,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":2},{"filename":"Edge.qml","from":15,"savedProperties":{"filename":"Edge.qml"},"to":14}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.08761438673812293,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":1024,"y":576}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":608,"y":320}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":608,"y":576}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":608,"y":448}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07261030906937009,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":800,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07261030906937009,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":832,"y":512}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":864,"y":737,"height":288,"maximumValue":50,"minimumValue":-100,"width":384}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":449,"y":738,"height":288,"maximumValue":50,"minimumValue":-100,"width":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07261030906937009,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":1024,"y":448}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.08761438673812293,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":1024,"y":320}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-160,"y":96,"height":226,"text":"Lateral inhibition 1:\n\nIn this example all 3 neurons are stimulated the same amount, and the center neuron is laterally inhibiting the neighbours so the output is dominated by the center.","width":356}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":544,"y":96,"height":160,"text":"Input layer\ncould be sensory neurons in the skin. ","width":224}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":960,"y":128,"height":120,"text":"Output layer","width":180}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-192,"y":736,"height":321,"text":"Exercise:\n\nLateral inhibition is often illustrated by a pencil touching the skin. Then the central neuron will be activated more strongly than the two neighbours. \n\nYou can simulate this by changing the synaptic strengths to the input layer. Check the signal-to-noise in the input and output layer (by counting action potentials).","width":488}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07015367823847792,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":192,"y":448}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":0,"y":448}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":1113.8385971478897,"width":1973.9791674883038,"x":-385.9097703692968,"y":59.35176702014807}}} \ No newline at end of file diff --git a/neuronify-core/examples/lateral_inhibition_2.nfy b/neuronify-core/examples/lateral_inhibition_2.nfy new file mode 100644 index 00000000..8e0ed99c --- /dev/null +++ b/neuronify-core/examples/lateral_inhibition_2.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/MeterEdge.qml","from":7,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0000019826358459990624,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.18402591023557607,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":5},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.18402591023557607,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":4},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.18402591023557607,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":8},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0000019826358459990624,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":9},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":9},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":8},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":7,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":7,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":5,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00010834322064402138,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":4,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00010834322064402138,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":9},{"filename":"edges/CurrentSynapse.qml","from":11,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00007187710616690275,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":8},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0000019826358459990624,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":11},{"filename":"edges/CurrentSynapse.qml","from":12,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00007187710616690275,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":8},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0000019826358459990624,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":12},{"filename":"edges/CurrentSynapse.qml","from":13,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.18402591023557607,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":16},{"filename":"edges/CurrentSynapse.qml","from":16,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00010834322064402138,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":13,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.18402591023557607,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":15},{"filename":"edges/CurrentSynapse.qml","from":17,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00010834322064402138,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":9},{"filename":"edges/CurrentSynapse.qml","from":14,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.18402591023557607,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":17},{"filename":"edges/CurrentSynapse.qml","from":21,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0045811926506062065,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":21,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0045811926506062065,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":21,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0045811926506062065,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":3},{"filename":"edges/CurrentSynapse.qml","from":21,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0045811926506062065,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":14},{"filename":"edges/CurrentSynapse.qml","from":21,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0045811926506062065,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":13},{"filename":"Edge.qml","from":22,"savedProperties":{"filename":"Edge.qml"},"to":21}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0733255024510552,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"A2","x":-160,"y":640}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.058198802322551674,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"C","x":352,"y":416}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.058198802322551674,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"A","x":-160,"y":416}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07375004607677803,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"B","x":96,"y":416}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":160,"y":544}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":32,"y":544}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":-160,"y":800,"height":384,"maximumValue":50,"minimumValue":-100,"width":576}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":654,"y":328,"height":239,"maximumValue":50,"minimumValue":-100,"width":461}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.05896098431498351,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"B2","x":96,"y":640}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0733255024510552,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"C2","x":352,"y":640}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":321,"y":-82,"height":298,"text":"Lateral inhibition 2:\n\nAll neurons in the network are inhibiting their neigbours. The center neuron is stimulated more strongly than the others, and lateral inhibition result in output even more dominated by the center.\n \nThis wiring is found in both sensory information in the skin and the eye, and makes our senses most sensitive to change in stimuli strength (e.g. contrasts).\n","width":635}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0714824733878793,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":288,"y":544}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0714824733878793,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":-96,"y":544}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07375004607677803,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":-352,"y":416}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07375004607677803,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":480,"y":416}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":-352,"y":640}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":-288,"y":544}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":448,"y":544}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-273,"y":132,"height":120,"text":"This neuron represent the stimulus","width":180}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-782,"y":360,"height":124,"text":"\"Sensory neurons\"\nInput layers","width":272}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-783,"y":581,"height":116,"text":"Output layer","width":265}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0600913831870096,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":96,"y":224}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":96,"y":64}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":1256.6676746836827,"width":2227.105270578258,"x":-847.2416865762958,"y":-174.41500833722347}}} \ No newline at end of file diff --git a/neuronify-core/examples/leaky.nfy b/neuronify-core/examples/leaky.nfy new file mode 100644 index 00000000..55f29994 --- /dev/null +++ b/neuronify-core/examples/leaky.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/ImmediateFireSynapse.qml","from":6,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":5},{"filename":"edges/CurrentSynapse.qml","from":5,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0000977797566312293,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":2,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":3,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00000199999999999994,"initialPotential":-0.07,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06902784353172711,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"B","x":960,"y":640}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":960,"y":800}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1120,"y":544,"height":256,"maximumValue":50,"minimumValue":-100,"width":352}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":672,"y":768,"height":160,"text":"Connect the DC current source to drive neuron B towards firing.","width":224}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":672,"y":320,"height":224,"text":"The leaky integrate-and-fire neuron is driven towards its resting potential whenever it is not stimulated by other currents or synaptic input.\n\nTouch the sensor to make neuron A send synaptic input to neuron B.","width":416}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.07,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07046647340305258,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"A","x":821.0449009872046,"y":639.164809127248}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":643.7750007071888,"y":619.6009421065892}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":725.707065367093,"width":1188.2922707765267,"x":528.8249279661167,"y":292.1465660959973}}} diff --git a/neuronify-core/examples/prolonged_activity.nfy b/neuronify-core/examples/prolonged_activity.nfy new file mode 100644 index 00000000..c255f953 --- /dev/null +++ b/neuronify-core/examples/prolonged_activity.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":8.015261056176754e-13,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":3.058733899503953e-11,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":6,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.1003595575634234e-14,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":6,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.1003595575634234e-14,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":3},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.167254940594208e-9,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":6},{"filename":"edges/ImmediateFireSynapse.qml","from":4,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":6},{"filename":"edges/MeterEdge.qml","from":5,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":3,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07026345473059087,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":736,"y":512}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00028699999999999993,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07053681980808743,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":416,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00048799999999999994,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07037606876559209,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":512,"y":256}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0004909999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07026345473059087,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":608,"y":384}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":258.09396168615433,"y":491.3530655391121}},{"filename":"meters/SpikeDetector.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":896,"y":416,"height":256,"showLegend":true,"timeRange":0.101,"width":352}},{"filename":"neurons/AdaptationNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0005,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0699993852082577,"adaptation":1e-8,"timeConstant":0.5},"inhibitory":false,"label":"","x":512,"y":512}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":896,"y":128,"height":256,"text":"Hit the touch activator to see the network go.\n\nIt stops after some time because of the adaptive neuron.\n\nWe still think it's pretty cool.","width":352}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":712.9387315270939,"width":1167.3850574712646,"x":225.84156216630228,"y":64.14252859275048}}} diff --git a/neuronify-core/examples/reciprocal_inhibition.nfy b/neuronify-core/examples/reciprocal_inhibition.nfy new file mode 100644 index 00000000..63ccaffc --- /dev/null +++ b/neuronify-core/examples/reciprocal_inhibition.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":7,"savedProperties":{"filename":"Edge.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":8.108970533246285e-148,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.12851215656510334,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":3},{"filename":"edges/CurrentSynapse.qml","from":4,"savedProperties":{"engine":{"alphaFunction":false,"delay":0,"exponential":1.6534744415004012e-148,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":5,"savedProperties":{"engine":{"alphaFunction":false,"delay":0,"exponential":0.024894280619189486,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0,"exponential":5.927473103402207e-149,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":4},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0,"exponential":0.00892424915046724,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":5},{"filename":"edges/ImmediateFireSynapse.qml","from":13,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":4},{"filename":"edges/ImmediateFireSynapse.qml","from":12,"savedProperties":{"engine":{},"filename":"edges/ImmediateFireSynapse.qml"},"to":5},{"filename":"edges/MeterEdge.qml","from":9,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":9,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":10,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":2},{"filename":"edges/MeterEdge.qml","from":10,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":1},{"filename":"Edge.qml","from":6,"savedProperties":{"filename":"Edge.qml"},"to":1}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000014,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":544,"y":608}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07434630290509935,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":320,"y":608}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0678297681485959,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":320,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07948095451509891,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":544,"y":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0700000000000014,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":448,"y":544}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07414411264302075,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":448,"y":448}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":224,"y":736}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":224,"y":256}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-320,"y":160,"height":224,"text":"In reciprocal inhibition, only one pathway in the neural network gets to fire.\n\nThe other is kept down because of the inhibitory neurons.","width":320}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":704,"y":416,"height":224,"maximumValue":50,"minimumValue":-100,"width":288}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":-96,"y":416,"height":224,"maximumValue":50,"minimumValue":-100,"width":320}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":704,"y":128,"height":224,"text":"The good news is that we get to decide which side wins. Just hit one of the touch activators to flip the switch.\n\nWho's in charge now, huh?","width":288}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":430.63939856078014,"y":220.73008381806244}},{"filename":"sensors/TouchSensor.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":429.32978494918007,"y":717.0736426144754}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":919.793611982827,"width":1630.0866508775218,"x":-394.17110143421024,"y":58.818086408365964}}} \ No newline at end of file diff --git a/neuronify-core/examples/recurrent_inhibition.nfy b/neuronify-core/examples/recurrent_inhibition.nfy new file mode 100644 index 00000000..27c20cf5 --- /dev/null +++ b/neuronify-core/examples/recurrent_inhibition.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":3,"savedProperties":{"filename":"Edge.qml"},"to":7},{"filename":"edges/CurrentSynapse.qml","from":7,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0045811926506062065,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":8},{"filename":"edges/CurrentSynapse.qml","from":8,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.000002312448865431185,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":9},{"filename":"edges/CurrentSynapse.qml","from":9,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00008383391884170023,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":10},{"filename":"edges/CurrentSynapse.qml","from":10,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.0030392568040834523,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":8},{"filename":"edges/MeterEdge.qml","from":1,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":9},{"filename":"edges/MeterEdge.qml","from":0,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":7},{"filename":"edges/MeterEdge.qml","from":4,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":8}],"fileFormatVersion":4,"nodes":[{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":351.3920454545455,"y":147.10795454545456,"height":180,"maximumValue":50,"minimumValue":-100,"width":240}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":869.210227272727,"y":357.1523520084567,"height":180,"maximumValue":50,"minimumValue":-100,"width":240}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":0,"y":320,"height":192,"text":"The DC current source delivers a constant current to the excitatory input neuron.\n\nLook at it go!","width":288}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":316.1297305640255,"y":387.66373434668947}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":479.9381529398896,"y":615.680236260866,"height":180,"maximumValue":50,"minimumValue":-100,"width":240}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":832,"y":608,"height":192,"text":"If we compare the output signal to the input, we see that the firing rate is reduced.","width":320}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":736,"y":96,"height":192,"text":"The inhibitory neuron inhibits backwards in the network, reducing the strength of the signal.","width":288}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00030099999999999994,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0600913831870096,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Input","x":448.4545694888608,"y":388.1176506592844}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.00039999999999999996,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0702644402711086,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"A","x":565.896429953977,"y":389.28044135695876}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00039999999999999996,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07150494524516508,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Output","x":698.4545694888606,"y":388.1176506592845}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":-0.000242,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07214032100080597,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"B","x":642.6406160004885,"y":270.67579019416814}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":937.6795366957282,"width":1680.142514557192,"x":-125.68828928361997,"y":1.7809501951313829}}} \ No newline at end of file diff --git a/neuronify-core/examples/refractory_period.nfy b/neuronify-core/examples/refractory_period.nfy new file mode 100644 index 00000000..cbd00206 --- /dev/null +++ b/neuronify-core/examples/refractory_period.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":3,"savedProperties":{"filename":"Edge.qml"},"to":1},{"filename":"Edge.qml","from":3,"savedProperties":{"filename":"Edge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":4,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":1},{"filename":"edges/MeterEdge.qml","from":5,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":2,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":2,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":1}],"fileFormatVersion":3,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":9.999999999999999e-10,"fireOutput":0.00009999999999999994,"initialPotential":0,"restingPotential":0,"threshold":0.020000000000000004,"voltage":0.018316073862546144,"refractoryPeriod":0.01,"resistance":20000000},"inhibitory":false,"label":"with refractory period ","x":128,"y":224}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":9.999999999999999e-10,"fireOutput":0.00009999999999999994,"initialPotential":0,"restingPotential":0,"threshold":0.020000000000000004,"voltage":0.006443204906717689,"refractoryPeriod":0,"resistance":20000000},"inhibitory":false,"label":"without refractory period ","x":128,"y":512}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":416,"y":192,"height":448,"maximumValue":30,"minimumValue":-10,"width":352}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-9},"inhibitory":false,"label":"","x":128,"y":384}},{"filename":"meters/RatePlot.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":0,"y":640,"height":224,"maximumValue":233,"minimumValue":0,"showLegend":true,"temporalResolution":0.2,"width":352,"windowDuration":0.3}},{"filename":"meters/RatePlot.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":0,"y":-64,"height":224,"maximumValue":100,"minimumValue":0,"showLegend":true,"temporalResolution":0.2,"width":352,"windowDuration":0.3}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-384,"y":160,"height":544,"text":"The refractory period sets a upper bound for firing frequency with increasing input. Try to increase the input current and observe how the firing rate of the top cell increases gradually with current, approaching a maximum rate of 100 Hz. Can you tell from the firing rate plot what the refractory period of this cell is?\n\nThis example is similar to figure 8.5b in the book \"Principles of Computational Modelling in Neuroscience\" by Sterratt, David et. al.\n","width":352}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":1017.7669840864069,"width":1666.5190382701398,"x":-599.6779080764923,"y":-106.05918646003043}}} \ No newline at end of file diff --git a/neuronify-core/examples/rythm_transformation.nfy b/neuronify-core/examples/rythm_transformation.nfy new file mode 100644 index 00000000..ceadf8c9 --- /dev/null +++ b/neuronify-core/examples/rythm_transformation.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.56880009227646,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":0,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.81450625,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.000018941061807026416,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":4},{"filename":"edges/CurrentSynapse.qml","from":4,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.000531336899820569,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":10,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.048494525249423236,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"Edge.qml","from":8,"savedProperties":{"filename":"Edge.qml"},"to":3},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.048494525249423236,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":2},{"filename":"Edge.qml","from":7,"savedProperties":{"filename":"Edge.qml"},"to":10},{"filename":"Edge.qml","from":12,"savedProperties":{"filename":"Edge.qml"},"to":4},{"filename":"Edge.qml","from":11,"savedProperties":{"filename":"Edge.qml"},"to":1},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":2},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":5,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":5,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":10}],"fileFormatVersion":3,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.05587613286463298,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Output","x":832,"y":-320}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06258689889530501,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":736,"y":-480}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.055737551385037616,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Output","x":1504,"y":-320}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06530160797068602,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Input","x":1376,"y":-320}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.00020000000000000004,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07952695675522144,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":1408,"y":-480}},{"filename":"meters/SpikeDetector.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":608,"y":-192,"height":192,"showLegend":true,"timeRange":0.101,"width":320}},{"filename":"meters/SpikeDetector.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1280,"y":-192,"height":192,"showLegend":true,"timeRange":0.101,"width":320}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":608,"y":-320}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":1280,"y":-320}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":960,"y":-192,"height":192,"text":"Transformation of regular spike train:\n\nChange in spike frequency after their passage through synapse","width":288}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06530160797068602,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"Input","x":704,"y":-320}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":608,"y":-480}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":1280,"y":-480}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":717.7087401976986,"width":1175.1955979845359,"x":569.3120121064751,"y":-599.0190541847272}}} diff --git a/neuronify-core/examples/tutorial_1_intro.nfy b/neuronify-core/examples/tutorial_1_intro.nfy new file mode 100644 index 00000000..0bca74c6 --- /dev/null +++ b/neuronify-core/examples/tutorial_1_intro.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":1,"savedProperties":{"filename":"Edge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":2,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.000002,"initialPotential":-0.07,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06461135287974859,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":960,"y":640}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":704,"y":640}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1177,"y":572,"height":192,"maximumValue":50,"minimumValue":-100,"width":352}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":576,"y":832,"height":192,"text":"A constant current source.\n \nIt never stops pushing current into the neurons.","width":256}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":864,"y":832,"height":349,"text":"A leaky neuron.\n\nBased on the integrate-and-fire model, it fires once its membrane potential is above the threshold.\n\nThe color of the neuron shows it's state, the neuron is white while firing and grey when it is inhibited.","width":266}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":1171,"y":818,"height":296,"text":"A voltmeter.\n\nDisplays the membrane potential of the neuron. \n\nDon't be fooled by the shape of the action potential. This is not what an action potential really looks like. This is how it is represented in the integrate-and-fire model.","width":364}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":800,"y":352,"height":192,"text":"Welcome to Neuronify!\n\nThis is the simplest circuit we could think of. It has a single neuron driven by a current source and connected to a voltmeter.","width":384}},{"filename":"annotations/NextTutorial.qml","savedProperties":{"inhibitory":false,"label":"","x":1582,"y":837,"targetSimulation":"qrc:/simulations/tutorial/tutorial_2_circuits/tutorial_2_circuits.nfy"}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":892.810822209076,"width":1337.3220095041181,"x":542.3999187964421,"y":321.73985780876706}}} diff --git a/neuronify-core/examples/tutorial_2_circuits.nfy b/neuronify-core/examples/tutorial_2_circuits.nfy new file mode 100644 index 00000000..4a35ca83 --- /dev/null +++ b/neuronify-core/examples/tutorial_2_circuits.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":1,"savedProperties":{"filename":"Edge.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":0,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.03386553563803231,"linear":0,"maximumCurrent":2e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":3},{"filename":"edges/MeterEdge.qml","from":6,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":4}],"fileFormatVersion":4,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.07,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.058402387467967214,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"A","x":864,"y":640}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":704,"y":640}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":800,"y":352,"height":192,"text":"Neurons can be connected to each other to form circuits.\n\nOnce neuron A fires, it stimulates B by means of a synaptic connection.","width":384}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":1.9999999999999998e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.07,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.0689796593444307,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":101000000},"inhibitory":false,"label":"B","x":1024,"y":640}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.07,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06999607893274189,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"C","x":1184,"y":640}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":928,"y":800,"height":241,"text":"Touch neuron B to reveal its connection handle.\n\nThen drag the handle to neuron C to make a synaptic connection from neuron B to neuron C.","width":257}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1312,"y":576,"height":192,"maximumValue":50,"minimumValue":-100,"width":352}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":1344,"y":800,"height":192,"text":"Once B and C are connected, the membrane potential of C should change in the above plot.","width":288}},{"filename":"annotations/NextTutorial.qml","savedProperties":{"inhibitory":false,"label":"","x":1664,"y":800,"targetSimulation":"qrc:/simulations/tutorial/tutorial_3_creation/tutorial_3_creation.nfy"}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":901.4444377424182,"width":1405.408792447701,"x":629.7572883319235,"y":261.8752200675589}}} \ No newline at end of file diff --git a/neuronify-core/examples/tutorial_3_creation.nfy b/neuronify-core/examples/tutorial_3_creation.nfy new file mode 100644 index 00000000..d91e1565 --- /dev/null +++ b/neuronify-core/examples/tutorial_3_creation.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":3},{"filename":"edges/CurrentSynapse.qml","from":3,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":4},{"filename":"edges/CurrentSynapse.qml","from":4,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":5},{"filename":"edges/CurrentSynapse.qml","from":5,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1}],"fileFormatVersion":4,"nodes":[{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":896,"y":288,"height":224,"text":"To add new items to the workspace, click the + to the right.\n\nThis will open a menu of categories. From here you can pull in items and connect them to your circuit.","width":320}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000695,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":1344,"y":704}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":1280,"y":288,"height":224,"text":"We think the best item is the touch activator. \n\nTry connecting it to one of the neurons. It will fire every time you touch the sensor. This can keep us entertained for hours.","width":352}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000695,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":1504,"y":704}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000695,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":1504,"y":544}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.0002999999999999999,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07000000000000695,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":1344,"y":544}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":960,"y":832,"height":128,"text":"Tip: Some of the simulations under \"Examples\" will give you a walkthrough of the different items.\n\nBut feel free to build whatever you want. We won't judge.","width":608}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":740.4382504688097,"width":1281.7670554334015,"x":673.6400456434876,"y":249.59013266290793}}} diff --git a/neuronify-core/examples/two_neuron_oscillator.nfy b/neuronify-core/examples/two_neuron_oscillator.nfy new file mode 100644 index 00000000..5dc5bc2a --- /dev/null +++ b/neuronify-core/examples/two_neuron_oscillator.nfy @@ -0,0 +1 @@ +{"edges":[{"from":0,"to":1},{"from":1,"to":0},{"from":2,"to":0},{"from":3,"to":1},{"from":0,"to":4},{"from":1,"to":4}],"fileFormatVersion":2,"nodes":[{"fileName":"neurons/LeakyNeuron.qml","label":"","x":544,"y":608,"engine":{"capacitance":0.000001001,"fireOutput":-0.000010000000000000026,"initialPotential":-0.08,"restingPotential":-0.0012999999999999956,"synapticConductance":0,"synapticPotential":0.04999999999999999,"synapticTimeConstant":0.01,"threshold":0,"voltage":-0.03261499124171272},"refractoryPeriod":0,"resistance":10000},{"fileName":"neurons/LeakyNeuron.qml","label":"","x":768,"y":608,"engine":{"capacitance":0.000001001,"fireOutput":-0.000010000000000000026,"initialPotential":-0.08,"restingPotential":-0.0012999999999999956,"synapticConductance":-0.0000046588077516979476,"synapticPotential":0.04999999999999999,"synapticTimeConstant":0.01,"threshold":0,"voltage":-0.0042811343938620175},"refractoryPeriod":0,"resistance":10000},{"fileName":"generators/CurrentClamp.qml","label":"","x":384,"y":608,"engine":{"currentOutput":0.000001}},{"fileName":"generators/CurrentClamp.qml","label":"","x":960,"y":608,"engine":{"currentOutput":0.000001}},{"fileName":"meters/SpikeDetector.qml","label":"","x":512,"y":96,"height":320,"showLegend":true,"timeRange":0.1,"width":352},{"fileName":"annotations/Note.qml","label":"","x":960,"y":160,"height":192,"text":"Once in a while, inhibitory neurons can be fun too.\n\nEspecially when they dance.","width":320}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":772.8581596997542,"width":1261.8092403261294,"x":173.04195769895634,"y":54.95816591772989}}} diff --git a/neuronify-core/examples/types_of_inhibition.nfy b/neuronify-core/examples/types_of_inhibition.nfy new file mode 100644 index 00000000..d9bc45e1 --- /dev/null +++ b/neuronify-core/examples/types_of_inhibition.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"Edge.qml","from":3,"savedProperties":{"filename":"Edge.qml"},"to":1},{"filename":"Edge.qml","from":3,"savedProperties":{"filename":"Edge.qml"},"to":2},{"filename":"edges/CurrentSynapse.qml","from":1,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":3.110677460253162e-8,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":0},{"filename":"edges/CurrentSynapse.qml","from":2,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00047953155208806347,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":4,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00047953155208806347,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":5},{"filename":"edges/CurrentSynapse.qml","from":6,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":0.00047953155208806347,"linear":0,"maximumCurrent":1e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":5},{"filename":"Edge.qml","from":8,"savedProperties":{"filename":"Edge.qml"},"to":4},{"filename":"Edge.qml","from":8,"savedProperties":{"filename":"Edge.qml"},"to":6},{"filename":"edges/MeterEdge.qml","from":7,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0},{"filename":"edges/MeterEdge.qml","from":9,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":5},{"filename":"edges/MeterEdge.qml","from":11,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":5},{"filename":"edges/MeterEdge.qml","from":10,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":0}],"fileFormatVersion":3,"nodes":[{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07105614907687227,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":512,"y":288}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07952826231037095,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":352,"y":288}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.00005000000000000002,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.056114817425836835,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":352,"y":96}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":160,"y":288}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.056114817425836835,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":320,"y":608}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0.00019999999999999998,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.07307161638187129,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":512,"y":608}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":-0.00005000000000000002,"initialPotential":-0.08,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.056114817425836835,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":true,"label":"","x":320,"y":448}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":640,"y":192,"height":256,"maximumValue":50,"minimumValue":-100,"width":256}},{"filename":"generators/CurrentClamp.qml","savedProperties":{"engine":{"currentOutput":3e-10},"inhibitory":false,"label":"","x":160,"y":608}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":640,"y":512,"height":256,"maximumValue":50,"minimumValue":-100,"width":256}},{"filename":"meters/RatePlot.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":928,"y":192,"height":256,"maximumValue":50,"minimumValue":0,"showLegend":true,"temporalResolution":0.3,"width":288,"windowDuration":0.4}},{"filename":"meters/RatePlot.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":928,"y":512,"height":256,"maximumValue":50,"minimumValue":0,"showLegend":true,"temporalResolution":0.3,"width":288,"windowDuration":0.4}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-32,"y":256,"height":128,"text":"Presynaptic inhibition","width":160}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":-32,"y":576,"height":128,"text":"Postsynaptic inhibition","width":160}}],"workspace":{"playbackSpeed":2,"visibleRectangle":{"height":893.1696428571435,"width":1462.500000000001,"x":-71.52480240069399,"y":18.43966591592272}}} \ No newline at end of file diff --git a/neuronify-core/examples/visualInput.nfy b/neuronify-core/examples/visualInput.nfy new file mode 100644 index 00000000..5c7b4139 --- /dev/null +++ b/neuronify-core/examples/visualInput.nfy @@ -0,0 +1 @@ +{"edges":[{"filename":"edges/CurrentSynapse.qml","from":0,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":2.0805670583315266e-235,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1},{"filename":"edges/MeterEdge.qml","from":5,"savedProperties":{"filename":"edges/MeterEdge.qml"},"to":1},{"filename":"edges/CurrentSynapse.qml","from":6,"savedProperties":{"engine":{"alphaFunction":false,"delay":0.005,"exponential":1.0048696777184701e-255,"linear":0,"maximumCurrent":3e-9,"tau":0.002,"triggers":{}},"filename":"edges/CurrentSynapse.qml"},"to":1}],"fileFormatVersion":4,"nodes":[{"filename":"sensors/Retina.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":544,"y":-640,"kernelType":"kernels/RectangularKernel.qml","plotKernel":true,"sensitivity":1000,"kernelProperties":{"orientation":4.71238898038469}}},{"filename":"neurons/LeakyNeuron.qml","savedProperties":{"engine":{"capacitance":2e-10,"fireOutput":0,"initialPotential":-0.08,"maximumVoltage":0.06,"minimumVoltage":-0.09,"restingPotential":-0.07,"threshold":-0.055,"voltage":-0.06999999999999862,"voltageClamped":true,"refractoryPeriod":0.002,"resistance":100000000},"inhibitory":false,"label":"","x":960,"y":-640}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":875,"y":-1192,"height":249,"text":"Visual input is a spike generator based on visual input from a camera connected to your device. This mimics a neuron with a visual receptive field.\n\nThe receptive field of the Visual input item is a pattern detector. When the input image matches the receptive field pattern, the number of generated spikes increases.\n\n","width":620}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":1474,"y":-800,"height":350,"text":"The type receptive field can be chosen from the properties panel. \n\nThe sensitivity of the receptive field determine how sensitive the field is to the optimal pattern. This means that a low sensitivity will reduce the number of spikes, even though the input matches the receptive field.","width":372}},{"filename":"annotations/Note.qml","savedProperties":{"inhibitory":false,"label":"","x":356,"y":-353,"height":224,"text":"The rate bar above shows how well the input matches the receptive field pattern: \n\n- If they overlap the bar will increase (orange). \n- If they are opposite the bar will decrease (gray)","width":608}},{"filename":"meters/Voltmeter.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":1088,"y":-704,"height":240,"maximumValue":50,"minimumValue":-100,"width":320}},{"filename":"sensors/Retina.qml","savedProperties":{"engine":{},"inhibitory":false,"label":"","x":544,"y":-928,"kernelType":"kernels/GaborKernel.qml","plotKernel":true,"sensitivity":1600,"kernelProperties":{"theta":0}}}],"workspace":{"playbackSpeed":1,"visibleRectangle":{"height":1123.9938490906202,"width":1991.9766186691802,"x":265.74388196002724,"y":-1273.44658730265}}} \ No newline at end of file diff --git a/neuronify-core/src/app.rs b/neuronify-core/src/app.rs new file mode 100644 index 00000000..996f660f --- /dev/null +++ b/neuronify-core/src/app.rs @@ -0,0 +1,1319 @@ +use crate::components::*; +use crate::constants::*; +use crate::measurement::voltmeter::{RollingWindow, VoltageSeries, Voltmeter}; +use crate::rendering::{ + collect_connections, collect_spheres, collect_voltmeter_traces, ConnectionData, Sphere, +}; +use crate::serialization::{LoadContext, SaveContext}; +use crate::tools::*; +use chrono::{DateTime, Duration, Utc}; +use glam::Vec3; +use hecs::serialize::column::*; +use hecs::Entity; +use postcard::ser_flavors::Flavor; +use std::cmp::Ordering; +use std::collections::HashSet; +use std::io::BufReader; +use std::io::Read; +use std::io::Write; +use std::path::PathBuf; +use std::thread; +use visula::winit::dpi::PhysicalPosition; +use visula::winit::event::{ElementState, Event, MouseButton, WindowEvent}; +use visula::{ + winit::keyboard::ModifiersKeyState, CustomEvent, InstanceBuffer, LineDelegate, Lines, + RenderData, Renderable, SphereDelegate, Spheres, +}; + +use crate::input::{Keyboard, Mouse}; +use crate::simulation; + +pub struct Neuronify { + pub tool: Tool, + pub previous_creation: Option, + pub connection_tool: Option, + pub stimulation_tool: Option, + pub world: hecs::World, + pub time: f64, + pub mouse: Mouse, + pub keyboard: Keyboard, + pub spheres: Spheres, + pub sphere_buffer: InstanceBuffer, + pub connection_lines: Lines, + pub connection_spheres: Spheres, + pub connection_buffer: InstanceBuffer, + pub iterations: u32, + pub last_update: DateTime, + pub fps: f64, + pub edit_enabled: bool, + pub last_touch_points: Option<((f64, f64), (f64, f64))>, + pub move_origin: Option, + pub active_entity: Option, + pub dragging_entity: Option, + pub drag_offset: Vec3, + pub resizing_voltmeter: Option<(Entity, ResizeCorner)>, +} + +#[derive(Debug)] +pub struct Error {} + +fn nearest( + mouse_position: &Vec3, + (_, x): &(Entity, &Position), + (_, y): &(Entity, &Position), +) -> Ordering { + mouse_position + .distance(x.position) + .partial_cmp(&mouse_position.distance(y.position)) + .unwrap_or(std::cmp::Ordering::Equal) +} + +fn within_selection_range( + mouse_position: Vec3, + (id, position): (Entity, &Position), +) -> Option<(Entity, Vec3)> { + if mouse_position.distance(position.position) < SELECTION_RANGE { + Some((id, position.position)) + } else { + None + } +} + +impl Neuronify { + pub fn new(application: &mut visula::Application) -> Neuronify { + application.camera_controller.enabled = false; + application.camera_controller.target_transform.center = Vec3::new(0.0, 0.0, 0.0); + application.camera_controller.target_transform.forward = + Vec3::new(0.3, -1.0, 0.0).normalize(); + application.camera_controller.target_transform.distance = 50.0; + application.camera_controller.current_transform = + application.camera_controller.target_transform.clone(); + + let sphere_buffer = InstanceBuffer::::new(&application.device); + let connection_buffer = InstanceBuffer::::new(&application.device); + let sphere = sphere_buffer.instance(); + let connection = connection_buffer.instance(); + + let spheres = Spheres::new( + &application.rendering_descriptor(), + &SphereDelegate { + position: sphere.position.clone(), + radius: sphere.radius, + color: sphere.color, + }, + ) + .unwrap(); + + let connection_vector = connection.position_b.clone() - connection.position_a.clone(); + let connection_endpoint = connection.position_a.clone() + connection_vector.clone() + - connection.directional.clone() * connection_vector.clone() + / connection_vector.clone().length() + * NODE_RADIUS + * 2.0; + let connection_lines = Lines::new( + &application.rendering_descriptor(), + &LineDelegate { + start: connection.position_a.clone(), + end: connection_endpoint.clone(), + width: connection.strength.clone() * 0.3, + color: connection.start_color.clone(), + }, + ) + .unwrap(); + + let connection_spheres = Spheres::new( + &application.rendering_descriptor(), + &SphereDelegate { + position: connection_endpoint, + radius: connection.directional.clone() * (0.5 * NODE_RADIUS), + color: Vec3::new(136.0 / 255.0, 57.0 / 255.0, 239.0 / 255.0).into(), + }, + ) + .unwrap(); + + let mut world = hecs::World::new(); + + #[cfg(not(target_arch = "wasm32"))] + { + let args: Vec = std::env::args().collect(); + if args.len() > 1 { + let path = &args[1]; + match std::fs::read_to_string(path) { + Ok(contents) => match crate::legacy::parse_legacy_nfy(&contents) { + Ok(sim) => { + log::info!( + "Loaded legacy simulation from {}: {} nodes, {} edges", + path, + sim.nodes.len(), + sim.edges.len() + ); + crate::legacy::convert::spawn_legacy_simulation(&mut world, &sim); + } + Err(e) => log::error!("Failed to parse legacy file {}: {}", path, e), + }, + Err(e) => log::error!("Failed to read file {}: {}", path, e), + } + } + } + + Neuronify { + spheres, + sphere_buffer, + connection_lines, + connection_spheres, + connection_buffer, + tool: Tool::Select, + previous_creation: None, + connection_tool: None, + stimulation_tool: None, + world, + time: 0.0, + mouse: Mouse { + left_down: false, + position: None, + delta_position: None, + }, + keyboard: Keyboard { shift_down: false }, + iterations: 4, + last_update: Utc::now(), + fps: 60.0, + edit_enabled: true, + last_touch_points: None, + move_origin: None, + active_entity: None, + dragging_entity: None, + drag_offset: Vec3::ZERO, + resizing_voltmeter: None, + } + } + + fn handle_tool(&mut self, application: &mut visula::Application) { + let Neuronify { + tool, + mouse, + connection_tool, + stimulation_tool, + world, + previous_creation, + move_origin, + active_entity, + dragging_entity, + drag_offset, + resizing_voltmeter, + .. + } = self; + if !mouse.left_down { + *stimulation_tool = None; + *connection_tool = None; + *previous_creation = None; + *move_origin = None; + *dragging_entity = None; + *resizing_voltmeter = None; + return; + } + let mouse_physical_position = match mouse.position { + Some(p) => p, + None => { + return; + } + }; + let ndc_x = 2.0 * mouse_physical_position.x as f32 / application.config.width as f32 - 1.0; + let ndc_y = 1.0 - 2.0 * mouse_physical_position.y as f32 / application.config.height as f32; + let ray_clip = glam::Vec4::new(ndc_x, ndc_y, -1.0, 1.0); + let aspect_ratio = application.config.width as f32 / application.config.height as f32; + let inv_projection = application + .camera_controller + .projection_matrix(aspect_ratio) + .inverse(); + + let ray_eye = inv_projection * ray_clip; + let ray_eye = glam::Vec4::new(ray_eye.x, ray_eye.y, -1.0, 0.0); + let inv_view_matrix = application.camera_controller.view_matrix().inverse(); + let ray_world = inv_view_matrix * ray_eye; + let ray_world = Vec3::new(ray_world.x, ray_world.y, ray_world.z).normalize(); + let ray_origin = application.camera_controller.position(); + let t = -ray_origin.y / ray_world.y; + let intersection = ray_origin + t * ray_world; + let mouse_position = intersection; + + let minimum_distance = match tool { + Tool::Axon => MIN_CREATION_DISTANCE_AXON, + _ => MIN_CREATION_DISTANCE_DEFAULT, + }; + let previous_too_near = if let Some(pc) = previous_creation { + if let Ok(position) = world.get::<&Position>(pc.entity) { + position.position.distance(mouse_position) < minimum_distance + } else { + false + } + } else { + false + }; + match tool { + Tool::ExcitatoryNeuron | Tool::InhibitoryNeuron => { + if previous_too_near { + return; + } + let neuron_type = if self.tool == Tool::InhibitoryNeuron { + NeuronType::Inhibitory + } else { + NeuronType::Excitatory + }; + let entity = world.spawn(( + Position { + position: mouse_position, + }, + LeakyNeuron::default(), + LeakyDynamics::default(), + LeakCurrent::default(), + neuron_type, + Deletable {}, + )); + if self.tool == Tool::InhibitoryNeuron { + world.insert_one(entity, Inhibitory).unwrap(); + } + self.previous_creation = Some(PreviousCreation { entity }); + } + Tool::CurrentSource => { + if previous_too_near { + return; + } + let entity = world.spawn(( + Position { + position: mouse_position, + }, + CurrentClamp::default(), + Deletable {}, + )); + self.previous_creation = Some(PreviousCreation { entity }); + } + Tool::TouchSensor => { + if previous_too_near { + return; + } + let entity = world.spawn(( + Position { + position: mouse_position, + }, + TouchSensor, + GeneratorDynamics::default(), + Deletable {}, + )); + self.previous_creation = Some(PreviousCreation { entity }); + } + Tool::RegularSpikeGenerator => { + if previous_too_near { + return; + } + let entity = world.spawn(( + Position { + position: mouse_position, + }, + RegularSpikeGenerator::default(), + GeneratorDynamics::default(), + Deletable {}, + )); + self.previous_creation = Some(PreviousCreation { entity }); + } + Tool::PoissonGenerator => { + if previous_too_near { + return; + } + let entity = world.spawn(( + Position { + position: mouse_position, + }, + PoissonGenerator::default(), + GeneratorDynamics::default(), + Deletable {}, + )); + self.previous_creation = Some(PreviousCreation { entity }); + } + Tool::StaticConnection => { + if let Some(ct) = connection_tool { + let target_candidates: Vec<(Entity, Vec3)> = world + .query::<&Position>() + .with::<&LeakyNeuron>() + .iter() + .map(|(e, p)| (e, p.position)) + .collect(); + let nearest_target = target_candidates + .iter() + .min_by(|a, b| { + a.1.distance(mouse_position) + .partial_cmp(&b.1.distance(mouse_position)) + .unwrap_or(Ordering::Equal) + }) + .and_then(|(id, pos)| { + if pos.distance(mouse_position) < 2.0 * NODE_RADIUS { + Some((*id, *pos)) + } else { + None + } + }); + if let Some((id, position)) = nearest_target { + let new_connection = Connection { + from: ct.from, + to: id, + strength: 1.0, + directional: true, + }; + let connection_exists = + world.query::<&Connection>().iter().any(|(_, c)| { + c.from == new_connection.from && c.to == new_connection.to + }); + if !connection_exists && ct.from != id { + world.spawn((new_connection, CurrentSynapse::default(), Deletable {})); + } + if !self.keyboard.shift_down { + ct.start = position; + ct.from = id; + } + } + ct.end = mouse_position; + } else { + let source_candidates: Vec<(Entity, Vec3)> = { + let mut candidates: Vec<_> = world + .query::<&Position>() + .with::<&LeakyNeuron>() + .iter() + .map(|(e, p)| (e, p.position)) + .collect(); + candidates.extend( + world + .query::<&Position>() + .with::<&CurrentClamp>() + .iter() + .map(|(e, p)| (e, p.position)), + ); + candidates.extend( + world + .query::<&Position>() + .with::<&GeneratorDynamics>() + .iter() + .map(|(e, p)| (e, p.position)), + ); + candidates + }; + *connection_tool = source_candidates + .iter() + .min_by(|a, b| { + a.1.distance(mouse_position) + .partial_cmp(&b.1.distance(mouse_position)) + .unwrap_or(Ordering::Equal) + }) + .and_then(|(id, pos)| { + if pos.distance(mouse_position) < 2.0 * NODE_RADIUS { + Some((*id, *pos)) + } else { + None + } + }) + .map(|(id, position)| ConnectionTool { + start: position, + end: mouse_position, + from: id, + }); + } + } + Tool::Stimulate => { + *stimulation_tool = Some(StimulationTool { + position: mouse_position, + }) + } + Tool::Erase => { + let to_delete = world + .query::<&Position>() + .with::<&Deletable>() + .iter() + .filter_map(|(entity, position)| { + let distance = position.position.distance(mouse_position); + if distance < NODE_RADIUS * 1.5 { + Some(entity) + } else { + None + } + }) + .collect::>(); + for entity in to_delete { + world.despawn(entity).unwrap(); + } + let connections_to_delete = world + .query::<&Connection>() + .with::<&Deletable>() + .iter() + .filter_map(|(entity, connection)| { + if let (Ok(from), Ok(to)) = ( + world.get::<&Position>(connection.from), + world.get::<&Position>(connection.to), + ) { + let a = from.position; + let b = to.position; + let p = mouse_position; + let ab = b - a; + let ap = p - a; + let t = ap.dot(ab) / ab.dot(ab); + let d = t * ab; + let point_on_line = a + d; + let distance_from_line = p.distance(point_on_line); + if distance_from_line < ERASE_RADIUS && (0.0..=1.0).contains(&t) { + Some(entity) + } else { + None + } + } else { + Some(entity) + } + }) + .collect::>(); + for connection in connections_to_delete { + world.despawn(connection).unwrap(); + } + } + Tool::Voltmeter => { + if previous_too_near { + return; + } + let result: Option<(Entity, Vec3)> = world + .query::<&Position>() + .with::<&LeakyNeuron>() + .iter() + .filter_map(|(entity, position)| { + let distance = position.position.distance(mouse_position); + if distance < NODE_RADIUS { + Some((entity, position.position)) + } else { + None + } + }) + .next() + .or_else(|| { + world + .query::<&Position>() + .with::<&Compartment>() + .iter() + .filter_map(|(entity, position)| { + let distance = position.position.distance(mouse_position); + if distance < NODE_RADIUS { + Some((entity, position.position)) + } else { + None + } + }) + .next() + }); + let Some((target, position)) = result else { + return; + }; + let voltmeter = world.spawn(( + Voltmeter {}, + Position { + position: position + + Vec3 { + x: 1.0, + y: 0.0, + z: 0.0, + }, + }, + VoltageSeries { + measurements: RollingWindow::new(100000), + spike_times: Vec::new(), + }, + Connection { + from: target, + to: Entity::DANGLING, + strength: 1.0, + directional: true, + }, + VoltmeterSize::default(), + Deletable {}, + )); + if let Ok(mut conn) = world.get::<&mut Connection>(voltmeter) { + conn.to = voltmeter; + } + *previous_creation = Some(PreviousCreation { entity: voltmeter }); + } + Tool::Select => match mouse.left_down { + true => { + if let Some(entity) = *dragging_entity { + if let Ok(mut pos) = world.get::<&mut Position>(entity) { + pos.position = mouse_position + *drag_offset; + pos.position.y = 0.0; + } + } else if let Some((entity, corner)) = *resizing_voltmeter { + let current = world + .get::<&Position>(entity) + .ok() + .map(|p| p.position) + .and_then(|vpos| { + world + .get::<&VoltmeterSize>(entity) + .ok() + .map(|s| (vpos, s.width, s.height)) + }); + if let Some((vpos, w, h)) = current { + let bl = vpos + Vec3::new(-h * 0.5, 0.0, 0.0); + let anchor = match corner { + ResizeCorner::TopLeft => bl + Vec3::new(0.0, 0.0, w), + ResizeCorner::TopRight => bl, + ResizeCorner::BottomLeft => bl + Vec3::new(h, 0.0, w), + ResizeCorner::BottomRight => bl + Vec3::new(h, 0.0, 0.0), + }; + let new_width = match corner { + ResizeCorner::TopRight | ResizeCorner::BottomRight => { + (mouse_position.z - anchor.z).max(2.0) + } + ResizeCorner::TopLeft | ResizeCorner::BottomLeft => { + (anchor.z - mouse_position.z).max(2.0) + } + }; + let new_height = match corner { + ResizeCorner::TopLeft | ResizeCorner::TopRight => { + (mouse_position.x - anchor.x).max(1.0) + } + ResizeCorner::BottomLeft | ResizeCorner::BottomRight => { + (anchor.x - mouse_position.x).max(1.0) + } + }; + let new_bl = match corner { + ResizeCorner::TopLeft => { + Vec3::new(anchor.x, 0.0, mouse_position.z.min(anchor.z - 2.0)) + } + ResizeCorner::TopRight => anchor, + ResizeCorner::BottomLeft => Vec3::new( + mouse_position.x.min(anchor.x - 1.0), + 0.0, + mouse_position.z.min(anchor.z - 2.0), + ), + ResizeCorner::BottomRight => { + Vec3::new(mouse_position.x.min(anchor.x - 1.0), 0.0, anchor.z) + } + }; + let new_pos = new_bl + Vec3::new(new_height * 0.5, 0.0, 0.0); + if let Ok(mut size) = world.get::<&mut VoltmeterSize>(entity) { + size.width = new_width; + size.height = new_height; + } + if let Ok(mut pos) = world.get::<&mut Position>(entity) { + pos.position = new_pos; + } + } + } else { + match *move_origin { + Some(origin) => { + let center = mouse_position - origin; + application.camera_controller.target_transform.center -= + Vec3::new(center.x, center.y, center.z); + application.camera_controller.current_transform.center = + application.camera_controller.target_transform.center; + } + None => { + let voltmeter_bounds: Vec<_> = world + .query::<(&Voltmeter, &Position)>() + .iter() + .filter_map(|(vid, (_, pos))| { + world.get::<&VoltmeterSize>(vid).ok().map(|size| { + (vid, pos.position, size.width, size.height) + }) + }) + .collect(); + + let mut found_corner = false; + let corner_threshold = 1.0_f32; + for (vid, vpos, w, h) in &voltmeter_bounds { + let bl = *vpos + Vec3::new(-h * 0.5, 0.0, 0.0); + let corners = [ + (bl + Vec3::new(*h, 0.0, 0.0), ResizeCorner::TopLeft), + (bl + Vec3::new(*h, 0.0, *w), ResizeCorner::TopRight), + (bl, ResizeCorner::BottomLeft), + (bl + Vec3::new(0.0, 0.0, *w), ResizeCorner::BottomRight), + ]; + for (corner_pos, corner_type) in &corners { + let dist = Vec3::new( + mouse_position.x - corner_pos.x, + 0.0, + mouse_position.z - corner_pos.z, + ) + .length(); + if dist < corner_threshold { + *resizing_voltmeter = Some((*vid, *corner_type)); + found_corner = true; + break; + } + } + if found_corner { + break; + } + } + + if !found_corner { + let mut found_voltmeter = false; + for (vid, vpos, w, h) in &voltmeter_bounds { + let bl = *vpos + Vec3::new(-h * 0.5, 0.0, 0.0); + if mouse_position.x >= bl.x + && mouse_position.x <= bl.x + h + && mouse_position.z >= bl.z + && mouse_position.z <= bl.z + w + { + *active_entity = Some(*vid); + *dragging_entity = Some(*vid); + *drag_offset = *vpos - mouse_position; + drag_offset.y = 0.0; + found_voltmeter = true; + break; + } + } + + if !found_voltmeter { + if let Some((entity, entity_pos)) = world + .query::<&Position>() + .iter() + .min_by(|a, b| nearest(&mouse_position, a, b)) + .and_then(|v| within_selection_range(mouse_position, v)) + { + *active_entity = Some(entity); + *dragging_entity = Some(entity); + *drag_offset = entity_pos - mouse_position; + drag_offset.y = 0.0; + } else { + *active_entity = None; + *move_origin = Some(mouse_position); + } + } + } + } + } + } + } + false => { + *move_origin = None; + *dragging_entity = None; + *resizing_voltmeter = None; + } + }, + Tool::Axon => match connection_tool { + None => { + let source_candidates: Vec<(Entity, Vec3)> = { + let mut candidates: Vec<_> = world + .query::<&Position>() + .with::<&StaticConnectionSource>() + .iter() + .map(|(e, p)| (e, p.position)) + .collect(); + candidates.extend( + world + .query::<&Position>() + .with::<&LeakyNeuron>() + .iter() + .map(|(e, p)| (e, p.position)), + ); + candidates.extend( + world + .query::<&Position>() + .with::<&CurrentClamp>() + .iter() + .map(|(e, p)| (e, p.position)), + ); + candidates.extend( + world + .query::<&Position>() + .with::<&GeneratorDynamics>() + .iter() + .map(|(e, p)| (e, p.position)), + ); + candidates + }; + *connection_tool = source_candidates + .iter() + .min_by(|a, b| { + a.1.distance(mouse_position) + .partial_cmp(&b.1.distance(mouse_position)) + .unwrap_or(Ordering::Equal) + }) + .and_then(|(id, pos)| { + if pos.distance(mouse_position) < 2.0 * NODE_RADIUS { + Some((*id, *pos)) + } else { + None + } + }) + .map(|(id, position)| ConnectionTool { + start: position, + end: mouse_position, + from: id, + }); + if let Some(ct) = connection_tool { + self.previous_creation = Some(PreviousCreation { entity: ct.from }); + } + } + Some(ct) => { + ct.end = mouse_position; + let target_candidates: Vec<(Entity, Vec3)> = world + .query::<&Position>() + .with::<&LeakyNeuron>() + .iter() + .map(|(e, p)| (e, p.position)) + .collect(); + let nearest_target = target_candidates + .iter() + .min_by(|a, b| { + a.1.distance(mouse_position) + .partial_cmp(&b.1.distance(mouse_position)) + .unwrap_or(Ordering::Equal) + }) + .and_then(|(id, pos)| { + if pos.distance(mouse_position) < 2.0 * NODE_RADIUS { + Some((*id, *pos)) + } else { + None + } + }); + + match nearest_target { + Some((id, position)) => { + let new_connection = Connection { + from: ct.from, + to: id, + strength: 1.0, + directional: true, + }; + let connection_exists = + world.query::<&Connection>().iter().any(|(_, c)| { + c.from == new_connection.from && c.to == new_connection.to + }); + if !connection_exists && ct.from != id { + world.spawn(( + new_connection, + Deletable {}, + CompartmentCurrent { + capacitance: COUPLING_CAPACITANCE, + }, + )); + } + if !self.keyboard.shift_down { + ct.start = position; + ct.from = id; + } + } + None => { + if previous_too_near { + return; + } + let neuron_type = + if let Ok(neuron_type) = world.get::<&NeuronType>(ct.from) { + Some((*neuron_type).clone()) + } else { + None + }; + if let Some(neuron_type) = neuron_type { + let compartment = world.spawn(( + Position { + position: mouse_position, + }, + neuron_type, + Compartment { + voltage: -10.0, + m: -0.625, + h: 0.0, + n: 0.0, + influence: 0.0, + capacitance: 1.0, + injected_current: 0.0, + fire_impulse: 0.0, + }, + StaticConnectionSource {}, + Deletable {}, + Selectable { selected: false }, + SpatialDynamics { + velocity: Vec3::new(0.0, 0.0, 0.0), + acceleration: Vec3::new(0.0, 0.0, 0.0), + }, + )); + let new_connection = Connection { + from: ct.from, + to: compartment, + strength: 1.0, + directional: false, + }; + world.spawn(( + new_connection, + Deletable {}, + CompartmentCurrent { + capacitance: COUPLING_CAPACITANCE, + }, + )); + self.previous_creation = Some(PreviousCreation { + entity: compartment, + }); + *connection_tool = Some(ConnectionTool { + start: mouse_position, + end: mouse_position, + from: compartment, + }); + } + } + } + } + }, + } + } + + pub fn save(&self, path: PathBuf) { + let mut context = SaveContext; + let mut serializer = postcard::Serializer { + output: postcard::ser_flavors::StdVec::new(), + }; + serialize(&self.world, &mut context, &mut serializer).unwrap(); + let mut writer = std::fs::File::create(path.with_extension("neuronify")).unwrap(); + writer + .write_all(&serializer.output.finalize().unwrap()) + .unwrap(); + } + + pub fn load_legacy_string(&mut self, contents: &str) { + match crate::legacy::parse_legacy_nfy(contents) { + Ok(sim) => { + self.world.clear(); + self.time = 0.0; + crate::legacy::convert::spawn_legacy_simulation(&mut self.world, &sim); + } + Err(e) => log::error!("Failed to parse legacy file: {}", e), + } + } + + pub fn loadfile(&mut self, path: PathBuf) { + let mut context = LoadContext::new(); + let reader = std::fs::File::open(path).unwrap(); + let mut bufreader = BufReader::new(reader); + let mut bytes: Vec = Vec::new(); + bufreader.read_to_end(&mut bytes).unwrap(); + let mut deserializer = postcard::Deserializer::from_bytes(&bytes); + self.world = deserialize(&mut context, &mut deserializer).unwrap(); + } + + pub fn from_slice(application: &mut visula::Application, bytes: &[u8]) -> Neuronify { + let mut neuronify = Neuronify::new(application); + let mut context = LoadContext::new(); + let mut deserializer = postcard::Deserializer::from_bytes(bytes); + neuronify.world = deserialize(&mut context, &mut deserializer).unwrap(); + neuronify.edit_enabled = false; + neuronify + } +} + +impl visula::Simulation for Neuronify { + type Error = Error; + fn clear_color(&self) -> wgpu::Color { + wgpu::Color { + r: crate::rendering::srgb_component(30) as f64, + g: crate::rendering::srgb_component(30) as f64, + b: crate::rendering::srgb_component(46) as f64, + a: 1.0, + } + } + fn update(&mut self, application: &mut visula::Application) { + let Neuronify { + connection_tool, + world, + time, + stimulation_tool, + .. + } = self; + + simulation::stimulate_nearby(world, stimulation_tool); + + let lif_dt = LIF_DT; + for _ in 0..self.iterations { + simulation::lif_step(world, lif_dt, *time); + *time += lif_dt; + } + + let recently_fired: HashSet = world + .query::<&LeakyDynamics>() + .iter() + .filter(|(_, d)| d.time_since_fire < self.iterations as f64 * lif_dt) + .map(|(e, _)| e) + .collect(); + + for _ in 0..self.iterations { + simulation::fhn_step(world, FHN_CDT, &recently_fired); + simulation::apply_spatial_forces(world); + simulation::integrate_motion(world, PHYSICS_DT); + } + + let spheres = collect_spheres(world); + let mut connections = collect_connections(world, &self.tool, connection_tool); + connections.extend(collect_voltmeter_traces(world)); + + self.sphere_buffer + .update(&application.device, &application.queue, &spheres); + + self.connection_buffer + .update(&application.device, &application.queue, &connections); + + let time_diff = Utc::now() - self.last_update; + #[cfg(not(target_arch = "wasm32"))] + if time_diff < Duration::milliseconds(TARGET_FRAME_MS) { + thread::sleep(std::time::Duration::from_millis( + (Duration::milliseconds(TARGET_FRAME_MS) - time_diff).num_milliseconds() as u64, + )) + } + let low_pass_factor = FPS_LOW_PASS_FACTOR; + let new_fps = 1.0 + / ((Utc::now() - self.last_update).num_nanoseconds().unwrap() as f64 * 1e-9) + .max(0.0000001); + self.fps = (1.0 - low_pass_factor) * self.fps + low_pass_factor * new_fps; + self.last_update = Utc::now(); + } + + fn render(&mut self, data: &mut RenderData) { + self.spheres.render(data); + self.connection_lines.render(data); + self.connection_spheres.render(data); + } + + fn gui(&mut self, _application: &visula::Application, context: &egui::Context) { + egui::Area::new("edit_button_area".into()) + .anchor(egui::Align2::RIGHT_BOTTOM, [-10.0, -10.0]) + .show(context, |ui| { + ui.toggle_value(&mut self.edit_enabled, "Edit").clicked(); + }); + if self.edit_enabled { + #[cfg(not(target_arch = "wasm32"))] + egui::TopBottomPanel::top("top_panel").show(context, |ui| { + egui::MenuBar::new().ui(ui, |ui| { + ui.menu_button("File", |ui| { + if ui.button("Save").clicked() { + if let Some(path) = rfd::FileDialog::new().save_file() { + self.save(path); + } + } + + if ui.button("Open").clicked() { + if let Some(path) = rfd::FileDialog::new().pick_file() { + self.loadfile(path); + } + } + }); + ui.menu_button("Examples", |ui| { + ui.menu_button("Tutorial", |ui| { + if ui.button("1 - Intro").clicked() { + self.load_legacy_string(include_str!( + "../examples/tutorial_1_intro.nfy" + )); + ui.close(); + } + if ui.button("2 - Circuits").clicked() { + self.load_legacy_string(include_str!( + "../examples/tutorial_2_circuits.nfy" + )); + ui.close(); + } + if ui.button("3 - Creation").clicked() { + self.load_legacy_string(include_str!( + "../examples/tutorial_3_creation.nfy" + )); + ui.close(); + } + }); + ui.menu_button("Neurons", |ui| { + if ui.button("Leaky").clicked() { + self.load_legacy_string(include_str!("../examples/leaky.nfy")); + ui.close(); + } + if ui.button("Inhibitory").clicked() { + self.load_legacy_string(include_str!("../examples/inhibitory.nfy")); + ui.close(); + } + if ui.button("Adaptation").clicked() { + self.load_legacy_string(include_str!("../examples/adaptation.nfy")); + ui.close(); + } + if ui.button("Burst").clicked() { + self.load_legacy_string(include_str!("../examples/burst.nfy")); + ui.close(); + } + }); + ui.menu_button("Circuits", |ui| { + if ui.button("Input Summation").clicked() { + self.load_legacy_string(include_str!( + "../examples/input_summation.nfy" + )); + ui.close(); + } + if ui.button("Prolonged Activity").clicked() { + self.load_legacy_string(include_str!( + "../examples/prolonged_activity.nfy" + )); + ui.close(); + } + if ui.button("Disinhibition").clicked() { + self.load_legacy_string(include_str!( + "../examples/disinhibition.nfy" + )); + ui.close(); + } + if ui.button("Recurrent Inhibition").clicked() { + self.load_legacy_string(include_str!( + "../examples/recurrent_inhibition.nfy" + )); + ui.close(); + } + if ui.button("Reciprocal Inhibition").clicked() { + self.load_legacy_string(include_str!( + "../examples/reciprocal_inhibition.nfy" + )); + ui.close(); + } + if ui.button("Lateral Inhibition").clicked() { + self.load_legacy_string(include_str!( + "../examples/lateral_inhibition.nfy" + )); + ui.close(); + } + if ui.button("Lateral Inhibition 1").clicked() { + self.load_legacy_string(include_str!( + "../examples/lateral_inhibition_1.nfy" + )); + ui.close(); + } + if ui.button("Lateral Inhibition 2").clicked() { + self.load_legacy_string(include_str!( + "../examples/lateral_inhibition_2.nfy" + )); + ui.close(); + } + if ui.button("Two Neuron Oscillator").clicked() { + self.load_legacy_string(include_str!( + "../examples/two_neuron_oscillator.nfy" + )); + ui.close(); + } + if ui.button("Rhythm Transformation").clicked() { + self.load_legacy_string(include_str!( + "../examples/rythm_transformation.nfy" + )); + ui.close(); + } + if ui.button("Types of Inhibition").clicked() { + self.load_legacy_string(include_str!( + "../examples/types_of_inhibition.nfy" + )); + ui.close(); + } + }); + ui.menu_button("Textbook", |ui| { + if ui.button("IF Response").clicked() { + self.load_legacy_string(include_str!( + "../examples/if_response.nfy" + )); + ui.close(); + } + if ui.button("Refractory Period").clicked() { + self.load_legacy_string(include_str!( + "../examples/refractory_period.nfy" + )); + ui.close(); + } + }); + ui.menu_button("Items", |ui| { + if ui.button("Generators").clicked() { + self.load_legacy_string(include_str!("../examples/generators.nfy")); + ui.close(); + } + }); + }); + }); + }); + egui::Window::new("Elements").show(context, |ui| { + for category in &TOOL_CATEGORIES { + ui.collapsing(category.label(), |ui| { + for (tool_value, label) in category.tools() { + ui.selectable_value(&mut self.tool, tool_value, label); + } + }); + } + }); + egui::Window::new("Settings").show(context, |ui| { + ui.label(format!("FPS: {:.0}", self.fps)); + ui.label("Simulation speed"); + ui.add(egui::Slider::new(&mut self.iterations, 1..=20)); + }); + if let Some(active_entity) = self.active_entity { + egui::Window::new("Selection").show(context, |ui| { + if let Ok(compartment) = self.world.get::<&Compartment>(active_entity) { + ui.collapsing("Compartment", |ui| { + egui::Grid::new("compartment_state").show(ui, |ui| { + ui.label("Voltage:"); + ui.label(format!("{:.2} mV", compartment.voltage)); + ui.end_row(); + ui.label("m:"); + ui.label(format!("{:.2}", compartment.m)); + ui.end_row(); + ui.label("n:"); + ui.label(format!("{:.2}", compartment.n)); + ui.end_row(); + ui.label("h:"); + ui.label(format!("{:.2}", compartment.h)); + ui.end_row(); + }); + }); + } + if let Ok(mut neuron) = self.world.get::<&mut LeakyNeuron>(active_entity) { + let is_inhibitory = self.world.get::<&Inhibitory>(active_entity).is_ok(); + let label = if is_inhibitory { + "LIF Neuron (Inhibitory)" + } else { + "LIF Neuron (Excitatory)" + }; + ui.collapsing(label, |ui| { + egui::Grid::new("neuron_settings").show(ui, |ui| { + ui.label("Threshold:"); + ui.add( + egui::Slider::new(&mut neuron.threshold, -0.08..=-0.03) + .suffix(" V"), + ); + ui.end_row(); + ui.label("Resting potential:"); + ui.add( + egui::Slider::new(&mut neuron.resting_potential, -0.09..=-0.05) + .suffix(" V"), + ); + ui.end_row(); + ui.label("Capacitance:"); + ui.add( + egui::Slider::new(&mut neuron.capacitance, 1e-11..=1e-9) + .suffix(" F"), + ); + ui.end_row(); + }); + }); + } + if let Ok(dynamics) = self.world.get::<&LeakyDynamics>(active_entity) { + ui.collapsing("Dynamics", |ui| { + egui::Grid::new("neuron_dynamics").show(ui, |ui| { + ui.label("Voltage:"); + ui.label(format!("{:.4} V", dynamics.voltage)); + ui.end_row(); + ui.label("Fired:"); + ui.label(format!("{}", dynamics.fired)); + ui.end_row(); + }); + }); + } + if let Ok(mut clamp) = self.world.get::<&mut CurrentClamp>(active_entity) { + ui.collapsing("Current Source", |ui| { + egui::Grid::new("clamp_settings").show(ui, |ui| { + ui.label("Current:"); + ui.add( + egui::Slider::new(&mut clamp.current_output, 0.0..=1e-8) + .suffix(" A"), + ); + ui.end_row(); + }); + }); + } + if let Ok(mut gen) = self.world.get::<&mut RegularSpikeGenerator>(active_entity) + { + ui.collapsing("Spike Generator", |ui| { + egui::Grid::new("spike_gen_settings").show(ui, |ui| { + ui.label("Frequency:"); + ui.add( + egui::Slider::new(&mut gen.frequency, 1.0..=200.0) + .suffix(" Hz"), + ); + ui.end_row(); + }); + }); + } + if let Ok(mut gen) = self.world.get::<&mut PoissonGenerator>(active_entity) { + ui.collapsing("Poisson Generator", |ui| { + egui::Grid::new("poisson_gen_settings").show(ui, |ui| { + ui.label("Rate:"); + ui.add(egui::Slider::new(&mut gen.rate, 1.0..=200.0).suffix(" Hz")); + ui.end_row(); + }); + }); + } + if self.world.get::<&Voltmeter>(active_entity).is_ok() { + ui.collapsing("Voltmeter", |ui| { + if let Ok(mut size) = + self.world.get::<&mut VoltmeterSize>(active_entity) + { + egui::Grid::new("voltmeter_size_settings").show(ui, |ui| { + ui.label("Width:"); + ui.add(egui::Slider::new(&mut size.width, 2.0..=20.0)); + ui.end_row(); + ui.label("Height:"); + ui.add(egui::Slider::new(&mut size.height, 1.0..=10.0)); + ui.end_row(); + }); + } + if let Ok(series) = self.world.get::<&VoltageSeries>(active_entity) { + if let Some(last) = series.measurements.last() { + ui.label(format!("Voltage: {:.2} mV", last.voltage)); + } + } + }); + } + }); + } + } + } + + fn handle_event(&mut self, application: &mut visula::Application, event: &Event) { + if let Event::WindowEvent { window_id, .. } = event { + if &application.window.id() != window_id { + return; + } + } + match event { + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state, + button: MouseButton::Left, + .. + }, + .. + } => { + self.mouse.left_down = *state == ElementState::Pressed; + self.mouse.delta_position = None; + self.handle_tool(application); + } + Event::WindowEvent { + event: WindowEvent::ModifiersChanged(state), + .. + } => { + self.keyboard.shift_down = state.lshift_state() == ModifiersKeyState::Pressed + || state.rshift_state() == ModifiersKeyState::Pressed; + } + Event::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + .. + } => { + self.mouse.delta_position = self.mouse.position.map(|previous| { + PhysicalPosition::new(position.x - previous.x, position.y - previous.y) + }); + self.mouse.position = Some(*position); + self.handle_tool(application); + } + Event::WindowEvent { + event: WindowEvent::MouseWheel { delta, .. }, + .. + } => { + let scroll = match delta { + winit::event::MouseScrollDelta::LineDelta(_, y) => *y, + winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 100.0, + }; + application.camera_controller.target_transform.distance *= 1.0 - scroll * 0.1; + application.camera_controller.target_transform.distance = application + .camera_controller + .target_transform + .distance + .clamp(CAMERA_MIN_DISTANCE, CAMERA_MAX_DISTANCE); + } + _ => {} + } + } +} diff --git a/neuronify-core/src/components.rs b/neuronify-core/src/components.rs new file mode 100644 index 00000000..6ae7f279 --- /dev/null +++ b/neuronify-core/src/components.rs @@ -0,0 +1,257 @@ +use glam::Vec3; +use hecs::Entity; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LeakyNeuron { + pub capacitance: f64, + pub resting_potential: f64, + pub threshold: f64, + pub initial_potential: f64, + pub voltage_clamped: bool, + pub minimum_voltage: f64, + pub maximum_voltage: f64, +} + +impl Default for LeakyNeuron { + fn default() -> Self { + Self { + capacitance: 2e-10, + resting_potential: -0.07, + threshold: -0.055, + initial_potential: -0.08, + voltage_clamped: true, + minimum_voltage: -0.09, + maximum_voltage: 0.06, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LeakyDynamics { + pub voltage: f64, + pub received_currents: f64, + pub fired: bool, + pub refractory_period: f64, + pub time_since_fire: f64, + pub enabled: bool, +} + +impl Default for LeakyDynamics { + fn default() -> Self { + Self { + voltage: -0.07, + received_currents: 0.0, + fired: false, + refractory_period: 0.002, + time_since_fire: f64::INFINITY, + enabled: true, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LeakCurrent { + pub resistance: f64, + pub current: f64, +} + +impl Default for LeakCurrent { + fn default() -> Self { + Self { + resistance: 1e8, + current: 0.0, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AdaptationCurrent { + pub adaptation: f64, + pub conductance: f64, + pub time_constant: f64, + pub current: f64, +} + +impl Default for AdaptationCurrent { + fn default() -> Self { + Self { + adaptation: 1e-8, + conductance: 0.0, + time_constant: 0.5, + current: 0.0, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CurrentClamp { + pub current_output: f64, +} + +impl Default for CurrentClamp { + fn default() -> Self { + Self { + current_output: 2e-9, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CurrentSynapse { + pub tau: f64, + pub maximum_current: f64, + pub delay: f64, + pub alpha_function: bool, + pub exponential: f64, + pub linear: f64, + pub triggers: Vec, + pub time: f64, + pub current_output: f64, +} + +impl Default for CurrentSynapse { + fn default() -> Self { + Self { + tau: 0.002, + maximum_current: 6e-9, + delay: 0.005, + alpha_function: false, + exponential: 0.0, + linear: 0.0, + triggers: Vec::new(), + time: 0.0, + current_output: 0.0, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ImmediateFireSynapse { + pub current_output: f64, +} + +impl Default for ImmediateFireSynapse { + fn default() -> Self { + Self { + current_output: 0.0, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Inhibitory; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TouchSensor; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct VoltmeterSize { + pub width: f32, + pub height: f32, +} + +impl Default for VoltmeterSize { + fn default() -> Self { + Self { + width: 8.0, + height: 4.0, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Annotation { + pub text: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct GeneratorDynamics { + pub fired: bool, + pub time_since_fire: f64, +} + +impl Default for GeneratorDynamics { + fn default() -> Self { + Self { + fired: false, + time_since_fire: f64::INFINITY, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RegularSpikeGenerator { + pub frequency: f64, +} + +impl Default for RegularSpikeGenerator { + fn default() -> Self { + Self { frequency: 20.0 } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PoissonGenerator { + pub rate: f64, +} + +impl Default for PoissonGenerator { + fn default() -> Self { + Self { rate: 20.0 } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum NeuronType { + Excitatory, + Inhibitory, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CompartmentCurrent { + pub capacitance: f64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Selectable { + pub selected: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct StaticConnectionSource {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Deletable {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Position { + pub position: Vec3, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SpatialDynamics { + pub velocity: Vec3, + pub acceleration: Vec3, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Connection { + pub from: Entity, + pub to: Entity, + pub strength: f64, + pub directional: bool, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct Compartment { + pub voltage: f64, + pub m: f64, + pub h: f64, + pub n: f64, + pub influence: f64, + pub capacitance: f64, + pub injected_current: f64, + #[serde(default)] + pub fire_impulse: f64, +} diff --git a/neuronify-core/src/constants.rs b/neuronify-core/src/constants.rs new file mode 100644 index 00000000..0c29584b --- /dev/null +++ b/neuronify-core/src/constants.rs @@ -0,0 +1,47 @@ +pub const FHN_TAU: f64 = 60.0; +pub const FHN_A: f64 = 0.7; +pub const FHN_B: f64 = 0.8; +pub const FHN_EPS: f64 = 0.08; +pub const FHN_SCALE: f64 = 50.0; +pub const FHN_OFFSET: f64 = 50.0; +pub const FHN_CDT: f64 = 0.01; + +pub const LIF_DT: f64 = 0.0001; +pub const PHYSICS_DT: f64 = 0.001; + +pub const NODE_RADIUS: f32 = 1.0; +pub const ERASE_RADIUS: f32 = 2.0 * NODE_RADIUS; +pub const ATTACHMENT_RANGE: f32 = 1.5 * NODE_RADIUS; +pub const SELECTION_RANGE: f32 = 0.9 * NODE_RADIUS; + +pub const VOLTMETER_TIME_WINDOW: f64 = 1.0 / 9.0; +pub const LIF_VOLTAGE_MIN: f64 = -100.0; +pub const LIF_VOLTAGE_MAX: f64 = 50.0; +pub const FHN_VOLTAGE_MIN: f64 = -80.0; +pub const FHN_VOLTAGE_MAX: f64 = 160.0; + +pub const REPULSION_STRENGTH: f32 = 5.0; +pub const SPRING_STRENGTH: f32 = 10.0; +pub const ANGLE_ALIGNMENT_STRENGTH: f32 = 1.0; + +pub const COUPLING_CAPACITANCE: f64 = 1.0 / 17.0; +pub const BRIDGE_CURRENT_SCALE: f64 = 10e-9; +pub const BRIDGE_VOLTAGE_THRESHOLD: f64 = 50.0; +pub const BRIDGE_VOLTAGE_CLAMP: f64 = 200.0; + +pub const COMPARTMENT_SPHERE_SCALE: f32 = 0.3; +pub const TRIGGER_SPHERE_SCALE: f32 = 0.5; + +pub const FHN_FIRE_VOLTAGE: f64 = 1.0; + +pub const BEZIER_SEGMENTS: usize = 16; +pub const BEZIER_BEND_FRACTION: f32 = 0.2; + +pub const MIN_CREATION_DISTANCE_AXON: f32 = 2.0 * NODE_RADIUS; +pub const MIN_CREATION_DISTANCE_DEFAULT: f32 = 6.0 * NODE_RADIUS; + +pub const FPS_LOW_PASS_FACTOR: f64 = 0.05; +pub const TARGET_FRAME_MS: i64 = 16; + +pub const CAMERA_MIN_DISTANCE: f32 = 5.0; +pub const CAMERA_MAX_DISTANCE: f32 = 200.0; diff --git a/neuronify-core/src/input.rs b/neuronify-core/src/input.rs new file mode 100644 index 00000000..6bf3c9c0 --- /dev/null +++ b/neuronify-core/src/input.rs @@ -0,0 +1,12 @@ +use visula::winit::dpi::PhysicalPosition; + +#[derive(Clone, Copy, Debug)] +pub struct Mouse { + pub left_down: bool, + pub position: Option>, + pub delta_position: Option>, +} + +pub struct Keyboard { + pub shift_down: bool, +} diff --git a/neuronify-core/src/legacy/convert.rs b/neuronify-core/src/legacy/convert.rs new file mode 100644 index 00000000..4e0e6df7 --- /dev/null +++ b/neuronify-core/src/legacy/convert.rs @@ -0,0 +1,207 @@ +use glam::Vec3; +use hecs::{Entity, World}; + +use crate::components::*; +use crate::measurement::voltmeter::{RollingWindow, VoltageSeries, Voltmeter}; +use crate::{Connection, Deletable, NeuronType, Position}; + +use super::{LegacyEdge, LegacyNode, LegacySimulation}; + +fn convert_position(x: f64, y: f64) -> Vec3 { + let scale = 50.0 / 2.0; + Vec3::new(-(y as f32 - 540.0) / scale, 0.0, (x as f32 - 960.0) / scale) +} + +pub fn spawn_legacy_simulation(world: &mut World, sim: &LegacySimulation) -> Vec { + let mut node_entities = Vec::new(); + + for node in &sim.nodes { + let entity = spawn_node(world, node); + node_entities.push(entity); + } + + for edge in &sim.edges { + if edge.from < node_entities.len() && edge.to < node_entities.len() { + spawn_edge(world, edge, &node_entities); + } + } + + node_entities +} + +fn spawn_node(world: &mut World, node: &LegacyNode) -> Entity { + let pos = Position { + position: convert_position(node.x, node.y), + }; + + match node.filename.as_str() { + "neurons/LeakyNeuron.qml" => spawn_leaky_neuron(world, node, pos), + "neurons/LeakyInhibitoryNeuron.qml" => { + let entity = spawn_leaky_neuron(world, node, pos); + world.insert_one(entity, Inhibitory).unwrap(); + entity + } + "neurons/AdaptationNeuron.qml" => spawn_adaptation_neuron(world, node, pos), + "generators/CurrentClamp.qml" => { + let current = node.engine.current_output.unwrap_or(2e-9); + world.spawn(( + pos, + CurrentClamp { + current_output: current, + }, + Deletable {}, + )) + } + "sensors/TouchSensor.qml" => { + world.spawn((pos, TouchSensor, GeneratorDynamics::default(), Deletable {})) + } + "meters/Voltmeter.qml" => world.spawn(( + pos, + Voltmeter {}, + VoltageSeries { + measurements: RollingWindow::new(10000), + spike_times: Vec::new(), + }, + VoltmeterSize::default(), + Deletable {}, + )), + "meters/SpikeDetector.qml" => world.spawn((pos,)), + s if s.starts_with("annotations/") => { + let text = node.text.clone().unwrap_or_default(); + world.spawn((pos, Annotation { text })) + } + _ => world.spawn((pos,)), + } +} + +fn spawn_leaky_neuron(world: &mut World, node: &LegacyNode, pos: Position) -> Entity { + let e = &node.engine; + + let neuron = LeakyNeuron { + capacitance: e.capacitance.unwrap_or(2e-10), + resting_potential: e.resting_potential.unwrap_or(-0.07), + threshold: e.threshold.unwrap_or(-0.055), + initial_potential: e.initial_potential.unwrap_or(-0.08), + voltage_clamped: e.voltage_clamped.unwrap_or(true), + minimum_voltage: e.minimum_voltage.unwrap_or(-0.09), + maximum_voltage: e.maximum_voltage.unwrap_or(0.06), + }; + + let dynamics = LeakyDynamics { + voltage: e.voltage.unwrap_or(neuron.resting_potential), + received_currents: 0.0, + fired: false, + refractory_period: e.refractory_period.unwrap_or(0.002), + time_since_fire: f64::INFINITY, + enabled: true, + }; + + let leak = LeakCurrent { + resistance: e.resistance.unwrap_or(1e8), + current: 0.0, + }; + + let neuron_type = if node.inhibitory { + NeuronType::Inhibitory + } else { + NeuronType::Excitatory + }; + + let entity = world.spawn((pos, neuron, dynamics, leak, neuron_type, Deletable {})); + + if node.inhibitory { + world.insert_one(entity, Inhibitory).unwrap(); + } + + entity +} + +fn spawn_adaptation_neuron(world: &mut World, node: &LegacyNode, pos: Position) -> Entity { + let entity = spawn_leaky_neuron(world, node, pos); + + let e = &node.engine; + let adapt = AdaptationCurrent { + adaptation: e.adaptation.unwrap_or(1e-8), + conductance: e.conductance.unwrap_or(0.0), + time_constant: e.time_constant.unwrap_or(0.5), + current: 0.0, + }; + + world.insert_one(entity, adapt).unwrap(); + entity +} + +fn spawn_edge(world: &mut World, edge: &LegacyEdge, node_entities: &[Entity]) { + let from = node_entities[edge.from]; + let to = node_entities[edge.to]; + + let connection = Connection { + from, + to, + strength: 1.0, + directional: true, + }; + + match edge.filename.as_str() { + "edges/CurrentSynapse.qml" => { + let e = &edge.engine; + let synapse = CurrentSynapse { + tau: e.tau.unwrap_or(0.002), + maximum_current: e.maximum_current.unwrap_or(3e-9), + delay: e.delay.unwrap_or(0.005), + alpha_function: e.alpha_function.unwrap_or(false), + exponential: e.exponential.unwrap_or(0.0), + linear: e.linear.unwrap_or(0.0), + triggers: Vec::new(), + time: 0.0, + current_output: 0.0, + }; + world.spawn((connection, synapse, Deletable {})); + } + "edges/ImmediateFireSynapse.qml" => { + world.spawn((connection, ImmediateFireSynapse::default(), Deletable {})); + } + "edges/MeterEdge.qml" => { + let (meter, neuron) = if world.get::<&Voltmeter>(from).is_ok() { + (from, to) + } else if world.get::<&Voltmeter>(to).is_ok() { + (to, from) + } else { + world.spawn((connection,)); + return; + }; + world + .insert( + meter, + (Connection { + from: neuron, + to: meter, + strength: 1.0, + directional: true, + },), + ) + .unwrap(); + } + "Edge.qml" => { + if world.get::<&LeakyDynamics>(to).is_err() { + return; + } + let e = &edge.engine; + let synapse = CurrentSynapse { + tau: e.tau.unwrap_or(0.002), + maximum_current: e.maximum_current.unwrap_or(3e-9), + delay: e.delay.unwrap_or(0.005), + alpha_function: e.alpha_function.unwrap_or(false), + exponential: e.exponential.unwrap_or(0.0), + linear: e.linear.unwrap_or(0.0), + triggers: Vec::new(), + time: 0.0, + current_output: 0.0, + }; + world.spawn((connection, synapse, Deletable {})); + } + _ => { + world.spawn((connection, Deletable {})); + } + } +} diff --git a/neuronify-core/src/legacy/mod.rs b/neuronify-core/src/legacy/mod.rs new file mode 100644 index 00000000..bb17ad15 --- /dev/null +++ b/neuronify-core/src/legacy/mod.rs @@ -0,0 +1,242 @@ +pub mod convert; +#[cfg(test)] +mod tests; + +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct LegacySimulation { + pub nodes: Vec, + pub edges: Vec, + pub file_format_version: Option, +} + +#[derive(Debug, Clone)] +pub struct LegacyNode { + pub filename: String, + pub x: f64, + pub y: f64, + pub engine: LegacyEngine, + pub inhibitory: bool, + pub label: String, + pub width: Option, + pub height: Option, + pub text: Option, + pub maximum_value: Option, + pub minimum_value: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct LegacyEngine { + pub capacitance: Option, + pub resting_potential: Option, + pub initial_potential: Option, + pub threshold: Option, + pub voltage: Option, + pub resistance: Option, + pub refractory_period: Option, + pub voltage_clamped: Option, + pub minimum_voltage: Option, + pub maximum_voltage: Option, + pub fire_output: Option, + pub current_output: Option, + pub inhibitory: Option, + pub adaptation: Option, + pub time_constant: Option, + pub conductance: Option, + pub tau: Option, + pub maximum_current: Option, + pub delay: Option, + pub alpha_function: Option, + pub exponential: Option, + pub linear: Option, +} + +#[derive(Debug, Clone)] +pub struct LegacyEdge { + pub filename: String, + pub from: usize, + pub to: usize, + pub engine: LegacyEngine, +} + +pub fn parse_legacy_nfy(json_str: &str) -> Result { + let root: Value = + serde_json::from_str(json_str).map_err(|e| format!("JSON parse error: {e}"))?; + + let file_format_version = root + .get("fileFormatVersion") + .and_then(|v| v.as_u64()) + .map(|v| v as u32); + let is_v2 = file_format_version.is_some_and(|v| v <= 2); + + let nodes = parse_nodes(&root, is_v2)?; + let edges = parse_edges(&root, is_v2)?; + + Ok(LegacySimulation { + nodes, + edges, + file_format_version, + }) +} + +fn parse_nodes(root: &Value, is_v2: bool) -> Result, String> { + let nodes_array = root + .get("nodes") + .and_then(|v| v.as_array()) + .ok_or("Missing 'nodes' array")?; + let mut result = Vec::new(); + + for node_val in nodes_array { + let filename = if is_v2 { + node_val + .get("fileName") + .or_else(|| node_val.get("filename")) + } else { + node_val + .get("filename") + .or_else(|| node_val.get("fileName")) + } + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let (props, engine_val) = if is_v2 { + (node_val, node_val.get("engine")) + } else { + let sp = node_val.get("savedProperties").unwrap_or(node_val); + (sp, sp.get("engine")) + }; + + let engine = parse_engine(engine_val); + + let x = get_f64(props, "x") + .or_else(|| get_f64(node_val, "x")) + .unwrap_or(0.0); + let y = get_f64(props, "y") + .or_else(|| get_f64(node_val, "y")) + .unwrap_or(0.0); + let inhibitory = props + .get("inhibitory") + .and_then(|v| v.as_bool()) + .or_else(|| node_val.get("inhibitory").and_then(|v| v.as_bool())) + .unwrap_or(false); + let label = props + .get("label") + .and_then(|v| v.as_str()) + .or_else(|| node_val.get("label").and_then(|v| v.as_str())) + .unwrap_or("") + .to_string(); + let text = props + .get("text") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let width = get_f64(props, "width"); + let height = get_f64(props, "height"); + let maximum_value = get_f64(props, "maximumValue"); + let minimum_value = get_f64(props, "minimumValue"); + + result.push(LegacyNode { + filename, + x, + y, + engine, + inhibitory, + label, + width, + height, + text, + maximum_value, + minimum_value, + }); + } + + Ok(result) +} + +fn parse_edges(root: &Value, is_v2: bool) -> Result, String> { + let edges_array = root + .get("edges") + .and_then(|v| v.as_array()) + .ok_or("Missing 'edges' array")?; + let mut result = Vec::new(); + + for edge_val in edges_array { + let from = edge_val + .get("from") + .and_then(|v| v.as_u64()) + .ok_or("Edge missing 'from'")? as usize; + let to = edge_val + .get("to") + .and_then(|v| v.as_u64()) + .ok_or("Edge missing 'to'")? as usize; + + let (filename, engine_val) = if is_v2 { + let fname = edge_val + .get("fileName") + .or_else(|| edge_val.get("filename")) + .and_then(|v| v.as_str()) + .unwrap_or("Edge.qml") + .to_string(); + (fname, edge_val.get("engine")) + } else { + let sp = edge_val.get("savedProperties"); + let fname = edge_val + .get("filename") + .and_then(|v| v.as_str()) + .or_else(|| sp.and_then(|s| s.get("filename")).and_then(|v| v.as_str())) + .unwrap_or("Edge.qml") + .to_string(); + let eng = sp + .and_then(|s| s.get("engine")) + .or_else(|| edge_val.get("engine")); + (fname, eng) + }; + + let engine = parse_engine(engine_val); + + result.push(LegacyEdge { + filename, + from, + to, + engine, + }); + } + + Ok(result) +} + +fn parse_engine(engine_val: Option<&Value>) -> LegacyEngine { + let Some(e) = engine_val else { + return LegacyEngine::default(); + }; + + LegacyEngine { + capacitance: get_f64(e, "capacitance"), + resting_potential: get_f64(e, "restingPotential"), + initial_potential: get_f64(e, "initialPotential"), + threshold: get_f64(e, "threshold"), + voltage: get_f64(e, "voltage"), + resistance: get_f64(e, "resistance"), + refractory_period: get_f64(e, "refractoryPeriod"), + voltage_clamped: e.get("voltageClamped").and_then(|v| v.as_bool()), + minimum_voltage: get_f64(e, "minimumVoltage"), + maximum_voltage: get_f64(e, "maximumVoltage"), + fire_output: get_f64(e, "fireOutput"), + current_output: get_f64(e, "currentOutput"), + inhibitory: e.get("inhibitory").and_then(|v| v.as_bool()), + adaptation: get_f64(e, "adaptation"), + time_constant: get_f64(e, "timeConstant"), + conductance: get_f64(e, "conductance"), + tau: get_f64(e, "tau"), + maximum_current: get_f64(e, "maximumCurrent"), + delay: get_f64(e, "delay"), + alpha_function: e.get("alphaFunction").and_then(|v| v.as_bool()), + exponential: get_f64(e, "exponential"), + linear: get_f64(e, "linear"), + } +} + +fn get_f64(val: &Value, key: &str) -> Option { + val.get(key).and_then(|v| v.as_f64()) +} diff --git a/neuronify-core/src/legacy/tests.rs b/neuronify-core/src/legacy/tests.rs new file mode 100644 index 00000000..78a9f0d7 --- /dev/null +++ b/neuronify-core/src/legacy/tests.rs @@ -0,0 +1,414 @@ +use super::convert::spawn_legacy_simulation; +use super::*; +use crate::components::*; +use crate::simulation::{lif_step, run_headless, SpikeRecord}; + +const EMPTY_NFY: &str = r#"{"nodes": [], "edges": []}"#; + +const TUTORIAL_1_INTRO_NFY: &str = include_str!("../../examples/tutorial_1_intro.nfy"); + +const TWO_NEURON_OSCILLATOR_NFY: &str = include_str!("../../examples/two_neuron_oscillator.nfy"); + +const LEAKY_NFY: &str = include_str!("../../examples/leaky.nfy"); + +const ADAPTATION_NFY: &str = include_str!("../../examples/adaptation.nfy"); + +const INHIBITORY_NFY: &str = include_str!("../../examples/inhibitory.nfy"); + +const TUTORIAL_2_CIRCUITS_NFY: &str = include_str!("../../examples/tutorial_2_circuits.nfy"); + +#[test] +fn test_parse_empty() { + let sim = parse_legacy_nfy(EMPTY_NFY).unwrap(); + assert_eq!(sim.nodes.len(), 0); + assert_eq!(sim.edges.len(), 0); +} + +#[test] +fn test_parse_tutorial_1_intro() { + let sim = parse_legacy_nfy(TUTORIAL_1_INTRO_NFY).unwrap(); + assert_eq!(sim.file_format_version, Some(4)); + assert_eq!(sim.nodes.len(), 8); + assert_eq!(sim.edges.len(), 2); + + assert_eq!(sim.nodes[0].filename, "neurons/LeakyNeuron.qml"); + assert!((sim.nodes[0].engine.capacitance.unwrap() - 2e-10).abs() < 1e-20); + + assert_eq!(sim.nodes[1].filename, "generators/CurrentClamp.qml"); + assert!((sim.nodes[1].engine.current_output.unwrap() - 3e-10).abs() < 1e-20); + + assert_eq!(sim.nodes[2].filename, "meters/Voltmeter.qml"); +} + +#[test] +fn test_parse_v2_format() { + let sim = parse_legacy_nfy(TWO_NEURON_OSCILLATOR_NFY).unwrap(); + assert_eq!(sim.file_format_version, Some(2)); + assert_eq!(sim.nodes.len(), 6); + assert_eq!(sim.edges.len(), 6); + + assert_eq!(sim.nodes[0].filename, "neurons/LeakyNeuron.qml"); + assert_eq!(sim.edges[0].filename, "Edge.qml"); +} + +#[test] +fn test_tutorial_1_intro_simulation() { + let sim = parse_legacy_nfy(TUTORIAL_1_INTRO_NFY).unwrap(); + let mut world = hecs::World::new(); + spawn_legacy_simulation(&mut world, &sim); + + let dt = 0.0001; + let steps = 10_000; + let spikes = run_headless(&mut world, steps, dt); + + assert!( + spikes.len() > 20, + "Expected at least 20 spikes in 1 second, got {}", + spikes.len() + ); + assert!( + spikes.len() < 100, + "Expected fewer than 100 spikes in 1 second, got {}", + spikes.len() + ); + + assert!(spikes.iter().all(|s| s.entity_index == 0)); +} + +#[test] +fn test_tutorial_2_circuits_simulation() { + let sim = parse_legacy_nfy(TUTORIAL_2_CIRCUITS_NFY).unwrap(); + let mut world = hecs::World::new(); + spawn_legacy_simulation(&mut world, &sim); + + let dt = 0.0001; + let steps = 10_000; + let spikes = run_headless(&mut world, steps, dt); + + let neuron_0_spikes: Vec<_> = spikes.iter().filter(|s| s.entity_index == 0).collect(); + let neuron_1_spikes: Vec<_> = spikes.iter().filter(|s| s.entity_index == 1).collect(); + + assert!( + neuron_0_spikes.len() > 5, + "Neuron 0 should fire, got {} spikes", + neuron_0_spikes.len() + ); + assert!( + neuron_1_spikes.len() > 2, + "Neuron 1 should fire from synaptic input, got {} spikes", + neuron_1_spikes.len() + ); +} + +#[test] +fn test_adaptation_decreasing_rate() { + let sim = parse_legacy_nfy(ADAPTATION_NFY).unwrap(); + let mut world = hecs::World::new(); + let entities = spawn_legacy_simulation(&mut world, &sim); + + let adapt_neuron_idx = sim + .nodes + .iter() + .position(|n| n.filename == "neurons/AdaptationNeuron.qml") + .unwrap(); + + let leaky_idx = sim + .nodes + .iter() + .position(|n| n.filename == "neurons/LeakyNeuron.qml") + .unwrap(); + + let leaky_entity = entities[leaky_idx]; + world + .insert_one( + leaky_entity, + CurrentClamp { + current_output: 5e-9, + }, + ) + .unwrap(); + + let dt = 0.0001; + let total_steps = 20_000; + + let mut all_spikes = Vec::new(); + let mut time = 0.0; + let neuron_entities: Vec = world + .query::<&LeakyNeuron>() + .iter() + .map(|(e, _)| e) + .collect(); + + for _ in 0..total_steps { + lif_step(&mut world, dt, time); + for (idx, entity) in neuron_entities.iter().enumerate() { + if let Ok(dynamics) = world.get::<&LeakyDynamics>(*entity) { + if dynamics.time_since_fire == 0.0 { + all_spikes.push(SpikeRecord { + entity_index: idx, + time, + }); + } + } + } + time += dt; + } + + let adapt_entity = entities[adapt_neuron_idx]; + let adapt_neuron_query_idx = neuron_entities + .iter() + .position(|&e| e == adapt_entity) + .unwrap(); + + let adapt_spikes: Vec<_> = all_spikes + .iter() + .filter(|s| s.entity_index == adapt_neuron_query_idx) + .collect(); + + if adapt_spikes.len() >= 4 { + let mid_time = time / 2.0; + let first_half_count = adapt_spikes.iter().filter(|s| s.time < mid_time).count(); + let second_half_count = adapt_spikes.iter().filter(|s| s.time >= mid_time).count(); + + assert!( + first_half_count > second_half_count, + "Adaptation neuron firing rate should decrease over time. \ + First half: {}, Second half: {}", + first_half_count, + second_half_count + ); + } +} + +#[test] +fn test_two_neuron_oscillator_v2() { + let sim = parse_legacy_nfy(TWO_NEURON_OSCILLATOR_NFY).unwrap(); + assert_eq!(sim.file_format_version, Some(2)); + + let mut world = hecs::World::new(); + spawn_legacy_simulation(&mut world, &sim); + + let dt = 0.0001; + let steps = 10_000; + let spikes = run_headless(&mut world, steps, dt); + + let n0_spikes: Vec<_> = spikes.iter().filter(|s| s.entity_index == 0).collect(); + let n1_spikes: Vec<_> = spikes.iter().filter(|s| s.entity_index == 1).collect(); + + assert!( + n0_spikes.len() > 3, + "Neuron 0 should fire in oscillator, got {}", + n0_spikes.len() + ); + assert!( + n1_spikes.len() > 3, + "Neuron 1 should fire in oscillator, got {}", + n1_spikes.len() + ); +} + +#[test] +fn test_inhibitory_simulation() { + let sim = parse_legacy_nfy(INHIBITORY_NFY).unwrap(); + let mut world = hecs::World::new(); + let entities = spawn_legacy_simulation(&mut world, &sim); + + assert_eq!(sim.nodes.len(), 9); + assert_eq!(sim.edges.len(), 5); + + let c_entity = entities[0]; + assert!(world.get::<&Inhibitory>(c_entity).is_err()); + + let b_entity = entities[3]; + assert!(world.get::<&Inhibitory>(b_entity).is_ok()); + + let dt = 0.0001; + for i in 0..100 { + lif_step(&mut world, dt, i as f64 * dt); + } +} + +#[test] +fn test_leaky_simulation() { + let sim = parse_legacy_nfy(LEAKY_NFY).unwrap(); + let mut world = hecs::World::new(); + spawn_legacy_simulation(&mut world, &sim); + + let dt = 0.0001; + let steps = 10_000; + let _spikes = run_headless(&mut world, steps, dt); + + assert!(!sim.nodes.is_empty()); +} + +#[test] +fn test_current_clamp_drives_neuron() { + use crate::{Connection, NeuronType, Position}; + use glam::Vec3; + + let mut world = hecs::World::new(); + + let neuron = world.spawn(( + LeakyNeuron::default(), + LeakyDynamics::default(), + LeakCurrent::default(), + NeuronType::Excitatory, + Position { + position: Vec3::ZERO, + }, + )); + + let clamp = world.spawn(( + CurrentClamp { + current_output: 5e-9, + }, + Position { + position: Vec3::new(0.0, 0.0, -1.0), + }, + )); + + world.spawn(( + Connection { + from: clamp, + to: neuron, + strength: 1.0, + directional: true, + }, + ImmediateFireSynapse::default(), + )); + + let dt = 0.0001; + let spikes = run_headless(&mut world, 10_000, dt); + + let neuron_spikes: Vec<_> = spikes.iter().filter(|s| s.entity_index == 0).collect(); + assert!( + neuron_spikes.len() > 10, + "Current clamp should drive neuron to fire repeatedly, got {} spikes", + neuron_spikes.len() + ); +} + +#[test] +fn test_inhibitory_suppresses_firing() { + use crate::{Connection, NeuronType, Position}; + use glam::Vec3; + + let mut world = hecs::World::new(); + + let target = world.spawn(( + LeakyNeuron::default(), + LeakyDynamics::default(), + LeakCurrent::default(), + NeuronType::Excitatory, + Position { + position: Vec3::ZERO, + }, + )); + + let clamp = world.spawn(( + CurrentClamp { + current_output: 5e-9, + }, + Position { + position: Vec3::new(0.0, 0.0, -2.0), + }, + )); + + world.spawn(( + Connection { + from: clamp, + to: target, + strength: 1.0, + directional: true, + }, + ImmediateFireSynapse::default(), + )); + + let inhibitor = world.spawn(( + LeakyNeuron::default(), + LeakyDynamics::default(), + LeakCurrent::default(), + NeuronType::Inhibitory, + Inhibitory, + Position { + position: Vec3::new(0.0, 0.0, 1.0), + }, + )); + + let inhib_clamp = world.spawn(( + CurrentClamp { + current_output: 10e-9, + }, + Position { + position: Vec3::new(0.0, 0.0, 2.0), + }, + )); + + world.spawn(( + Connection { + from: inhib_clamp, + to: inhibitor, + strength: 1.0, + directional: true, + }, + ImmediateFireSynapse::default(), + )); + + world.spawn(( + Connection { + from: inhibitor, + to: target, + strength: 1.0, + directional: true, + }, + CurrentSynapse { + maximum_current: 20e-9, + ..CurrentSynapse::default() + }, + )); + + let dt = 0.0001; + let spikes = run_headless(&mut world, 10_000, dt); + + let mut world_no_inhib = hecs::World::new(); + let neuron_alone = world_no_inhib.spawn(( + LeakyNeuron::default(), + LeakyDynamics::default(), + LeakCurrent::default(), + NeuronType::Excitatory, + Position { + position: Vec3::ZERO, + }, + )); + let clamp_alone = world_no_inhib.spawn(( + CurrentClamp { + current_output: 5e-9, + }, + Position { + position: Vec3::new(0.0, 0.0, -2.0), + }, + )); + world_no_inhib.spawn(( + Connection { + from: clamp_alone, + to: neuron_alone, + strength: 1.0, + directional: true, + }, + ImmediateFireSynapse::default(), + )); + let spikes_no_inhib = run_headless(&mut world_no_inhib, 10_000, dt); + + let target_spikes: Vec<_> = spikes.iter().filter(|s| s.entity_index == 0).collect(); + let alone_spikes: Vec<_> = spikes_no_inhib + .iter() + .filter(|s| s.entity_index == 0) + .collect(); + + assert!( + target_spikes.len() < alone_spikes.len(), + "Inhibition should reduce firing: {} with inhibition vs {} without", + target_spikes.len(), + alone_spikes.len() + ); +} diff --git a/neuronify-core/src/lib.rs b/neuronify-core/src/lib.rs index 43944d52..219f55ef 100644 --- a/neuronify-core/src/lib.rs +++ b/neuronify-core/src/lib.rs @@ -1,1707 +1,56 @@ -use crate::measurement::voltmeter::RollingWindow; -use crate::measurement::voltmeter::VoltageMeasurement; -use crate::measurement::voltmeter::VoltageSeries; -use crate::measurement::voltmeter::Voltmeter; -use crate::serialization::{LoadContext, SaveContext}; -use bytemuck::{Pod, Zeroable}; -use cgmath::prelude::*; -use cgmath::Vector4; -use chrono::{DateTime, Duration, Utc}; -use egui::Color32; -use egui::LayerId; -use egui::Pos2; -use egui_plot::PlotBounds; -use egui_plot::{Line, PlotPoints}; -use glam::Quat; -use glam::Vec3; -use hecs::serialize::column::*; -use hecs::Entity; +#[cfg(target_arch = "wasm32")] use js_sys::Uint8Array; -use postcard::ser_flavors::Flavor; -use serde::{Deserialize, Serialize}; +#[cfg(target_arch = "wasm32")] use std::borrow::BorrowMut; -use std::cmp::Ordering; -use std::collections::HashMap; -use std::io::BufReader; -use std::io::Read; -use std::io::Write; -use std::path::PathBuf; +#[cfg(target_arch = "wasm32")] use std::sync::Arc; -use std::thread; -use strum::EnumIter; -use strum::IntoEnumIterator; -use visula::create_window; +#[cfg(target_arch = "wasm32")] +use visula::winit::event::{Event, WindowEvent}; #[cfg(target_arch = "wasm32")] use visula::winit::platform::web::EventLoopExtWebSys; -use visula::winit::{ - dpi::PhysicalPosition, - event::{ElementState, Event, MouseButton, WindowEvent}, -}; -use visula::{ - create_event_loop, initialize_logger, winit::keyboard::ModifiersKeyState, Application, - CustomEvent, InstanceBuffer, LineDelegate, Lines, RenderData, Renderable, RunConfig, - Simulation, SphereDelegate, Spheres, Vector3, -}; -use visula_derive::Instance; +#[cfg(target_arch = "wasm32")] +use visula::{create_event_loop, initialize_logger, Application, CustomEvent, RunConfig}; +#[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; +#[cfg(target_arch = "wasm32")] use wasm_bindgen_futures::JsFuture; +#[cfg(target_arch = "wasm32")] use web_sys::{Request, RequestInit, Response}; +#[cfg(target_arch = "wasm32")] use winit::event_loop::EventLoop; -use winit::event_loop::EventLoopWindowTarget; +pub mod app; +pub mod components; +pub mod constants; +pub mod input; +pub mod legacy; pub mod measurement; +pub mod rendering; pub mod serialization; +pub mod simulation; +pub mod tools; -#[derive(Clone, Debug, EnumIter, PartialEq)] -pub enum Tool { - Select, - ExcitatoryNeuron, - InhibitoryNeuron, - CurrentSource, - Voltmeter, - StaticConnection, - LearningConnection, - Axon, - Erase, - Stimulate, -} - -const NODE_RADIUS: f32 = 1.0; -const ERASE_RADIUS: f32 = 2.0 * NODE_RADIUS; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum NeuronType { - Excitatory, - Inhibitory, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NeuronDynamics { - pub voltage: f64, - pub current: f64, - pub refraction: f64, - pub fired: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Neuron { - pub initial_voltage: f64, - pub reset_potential: f64, - pub resting_potential: f64, - pub threshold: f64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct SynapseCurrent { - pub current: f64, - pub tau: f64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CompartmentCurrent { - capacitance: f64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Selectable { - pub selected: bool, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CurrentSource { - pub current: f64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct StaticConnectionSource {} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct LearningSynapse {} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct PreviousCreation { - pub entity: Entity, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Deletable {} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Position { - pub position: Vec3, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct SpatialDynamics { - pub velocity: Vec3, - pub acceleration: Vec3, -} - -impl Default for Neuron { - fn default() -> Self { - Self::new() - } -} - -impl Neuron { - pub fn new() -> Neuron { - Neuron { - initial_voltage: 0.0, - reset_potential: -70.0, - resting_potential: -70.0, - threshold: 30.0, - } - } -} - -#[derive(Clone, Debug)] -pub struct ConnectionTool { - pub start: Vec3, - pub end: Vec3, - pub from: Entity, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Trigger { - pub total: f64, - pub remaining: f64, - pub current: f64, - pub connection: Entity, -} - -impl Trigger { - pub fn new(time: f64, current: f64, connection: Entity) -> Trigger { - Trigger { - total: time, - remaining: time, - current, - connection, - } - } - pub fn decrement(&mut self, dt: f64) { - self.remaining = (self.remaining - dt).max(0.0); - } - pub fn progress(&self) -> f64 { - (self.total - self.remaining) / self.total - } - pub fn done(&self) -> bool { - self.remaining <= 0.0 - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct LeakCurrent { - pub current: f64, - pub tau: f64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Connection { - pub from: Entity, - pub to: Entity, - pub strength: f64, - pub directional: bool, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct StimulateCurrent { - pub current: f64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct StimulationTool { - pub position: Vec3, -} - -#[repr(C, align(16))] -#[derive(Clone, Copy, Debug, Instance, Pod, Zeroable)] -pub struct Sphere { - pub position: glam::Vec3, - pub color: glam::Vec3, - pub radius: f32, - pub _padding: f32, -} - -#[derive(Clone, Copy, Debug)] -pub struct Mouse { - pub left_down: bool, - pub position: Option>, - pub delta_position: Option>, -} - -pub struct Keyboard { - pub shift_down: bool, -} - -pub struct Neuronify { - pub tool: Tool, - pub previous_creation: Option, - pub connection_tool: Option, - pub stimulation_tool: Option, - pub world: hecs::World, - pub time: f64, - pub mouse: Mouse, - pub keyboard: Keyboard, - pub spheres: Spheres, - pub sphere_buffer: InstanceBuffer, - pub connection_lines: Lines, - pub connection_spheres: Spheres, - pub connection_buffer: InstanceBuffer, - pub iterations: u32, - pub last_update: DateTime, - pub fps: f64, - pub edit_enabled: bool, - pub last_touch_points: Option<((f64, f64), (f64, f64))>, - pub move_origin: Option, - pub active_entity: Option, -} - -#[derive(Debug)] -pub struct Error {} - -#[repr(C, align(16))] -#[derive(Clone, Copy, Instance, Pod, Zeroable)] -pub struct ConnectionData { - pub start_color: Vec3, - pub end_color: Vec3, - pub position_a: Vec3, - pub position_b: Vec3, - pub strength: f32, - pub directional: f32, - pub _padding: [f32; 2], -} - -#[repr(C, align(16))] -#[derive(Clone, Copy, Instance, Pod, Zeroable)] -pub struct LineData { - pub start: Vec3, - pub end: Vec3, - pub _padding: [f32; 2], -} - -#[repr(C, align(16))] -#[derive(Clone, Copy, Instance, Pod, Zeroable)] -pub struct MeshInstanceData { - pub position: Vec3, - pub _padding: f32, - pub rotation: Quat, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -struct Compartment { - voltage: f64, - m: f64, - h: f64, - n: f64, - influence: f64, - capacitance: f64, - injected_current: f64, -} - -fn nearest( - mouse_position: &Vec3, - (_, x): &(Entity, &Position), - (_, y): &(Entity, &Position), -) -> Ordering { - mouse_position - .distance(x.position) - .partial_cmp(&mouse_position.distance(y.position)) - .unwrap_or(std::cmp::Ordering::Equal) -} - -fn within_attachment_range( - mouse_position: Vec3, - (id, position): (Entity, &Position), -) -> Option<(Entity, Vec3)> { - if mouse_position.distance(position.position) < 1.5 * NODE_RADIUS { - Some((id, position.position)) - } else { - None - } -} - -fn within_selection_range( - mouse_position: Vec3, - (id, position): (Entity, &Position), -) -> Option<(Entity, Vec3)> { - if mouse_position.distance(position.position) < 0.9 * NODE_RADIUS { - Some((id, position.position)) - } else { - None - } -} - -impl Neuronify { - pub fn new(application: &mut visula::Application) -> Neuronify { - application.camera_controller.enabled = false; - application.camera_controller.center = Vector3::new(0.0, 0.0, 0.0); - application.camera_controller.forward = Vector3::new(1.0, -1.0, 0.0); - application.camera_controller.distance = 50.0; - - let sphere_buffer = InstanceBuffer::::new(&application.device); - let connection_buffer = InstanceBuffer::::new(&application.device); - let sphere = sphere_buffer.instance(); - let connection = connection_buffer.instance(); - - let spheres = Spheres::new( - &application.rendering_descriptor(), - &SphereDelegate { - position: sphere.position.clone(), - radius: sphere.radius, - color: sphere.color, - }, - ) - .unwrap(); - - let connection_vector = connection.position_b.clone() - connection.position_a.clone(); - // TODO: Add normalize function to expressions - let connection_endpoint = connection.position_a.clone() + connection_vector.clone() - - connection.directional.clone() * connection_vector.clone() - / connection_vector.clone().length() - * NODE_RADIUS - * 2.0; - let connection_lines = Lines::new( - &application.rendering_descriptor(), - &LineDelegate { - start: connection.position_a.clone(), - end: connection_endpoint.clone(), - width: 0.3.into(), - start_color: connection.start_color.clone(), - end_color: connection.end_color.clone(), - }, - ) - .unwrap(); - - let connection_spheres = Spheres::new( - &application.rendering_descriptor(), - &SphereDelegate { - position: connection_endpoint, - radius: connection.directional.clone() * (0.5 * NODE_RADIUS), - color: Vec3::new(136.0 / 255.0, 57.0 / 255.0, 239.0 / 255.0).into(), - }, - ) - .unwrap(); - - let world = hecs::World::new(); - - Neuronify { - spheres, - sphere_buffer, - connection_lines, - connection_spheres, - connection_buffer, - tool: Tool::Select, - previous_creation: None, - connection_tool: None, - stimulation_tool: None, - world, - time: 0.0, - mouse: Mouse { - left_down: false, - position: None, - delta_position: None, - }, - keyboard: Keyboard { shift_down: false }, - iterations: 4, - last_update: Utc::now(), - fps: 60.0, - edit_enabled: true, - last_touch_points: None, - move_origin: None, - active_entity: None, - } - } - - fn handle_tool(&mut self, application: &mut visula::Application) { - let Neuronify { - tool, - mouse, - connection_tool, - stimulation_tool, - world, - previous_creation, - move_origin, - .. - } = self; - if !mouse.left_down { - *stimulation_tool = None; - *connection_tool = None; - *previous_creation = None; - *move_origin = None; - return; - } - let mouse_physical_position = match mouse.position { - Some(p) => p, - None => { - return; - } - }; - let screen_position = cgmath::Vector4 { - x: 2.0 * mouse_physical_position.x as f32 / application.config.width as f32 - 1.0, - y: 1.0 - 2.0 * mouse_physical_position.y as f32 / application.config.height as f32, - z: 1.0, - w: 1.0, - }; - let ray_clip = cgmath::Vector4 { - x: screen_position.x, - y: screen_position.y, - z: -1.0, - w: 1.0, - }; - let aspect_ratio = application.config.width as f32 / application.config.height as f32; - let inv_projection = application - .camera_controller - .projection_matrix(aspect_ratio) - .invert() - .unwrap(); - - let ray_eye = inv_projection * ray_clip; - let ray_eye = cgmath::Vector4 { - x: ray_eye.x, - y: ray_eye.y, - z: -1.0, - w: 0.0, - }; - let inv_view_matrix = application - .camera_controller - .view_matrix() - .invert() - .unwrap(); - let ray_world = inv_view_matrix * ray_eye; - let ray_world = cgmath::Vector3 { - x: ray_world.x, - y: ray_world.y, - z: ray_world.z, - } - .normalize(); - let ray_origin = application.camera_controller.position(); - let t = -ray_origin.y / ray_world.y; - let intersection = ray_origin + t * ray_world; - let mouse_position = Vec3::new(intersection.x, intersection.y, intersection.z); - - let minimum_distance = match tool { - Tool::Axon => 2.0 * NODE_RADIUS, - _ => 6.0 * NODE_RADIUS, - }; - let previous_too_near = if let Some(pc) = previous_creation { - if let Ok(position) = world.get::<&Position>(pc.entity) { - position.position.distance(mouse_position) < minimum_distance - } else { - false - } - } else { - false - }; - match tool { - Tool::ExcitatoryNeuron | Tool::InhibitoryNeuron => { - if previous_too_near { - return; - } - let dynamics = NeuronDynamics { - current: 0.0, - refraction: 0.0, - voltage: 0.0, - fired: false, - }; - let leak_current = LeakCurrent { - current: 0.0, - tau: 1.0, - }; - let stimulate_current = StimulateCurrent { current: 0.0 }; - let new_id = match self.tool { - Tool::ExcitatoryNeuron => Some(world.spawn(( - Position { - position: mouse_position + 0.4 * Vec3::Y, - }, - Neuron::new(), - NeuronType::Excitatory, - StaticConnectionSource {}, - dynamics, - leak_current, - Deletable {}, - stimulate_current, - Selectable { selected: false }, - ))), - Tool::InhibitoryNeuron => Some(world.spawn(( - Position { - position: mouse_position, - }, - Neuron::new(), - NeuronType::Inhibitory, - StaticConnectionSource {}, - dynamics, - leak_current, - Deletable {}, - stimulate_current, - Selectable { selected: false }, - ))), - _ => None, - }; - if let Some(id) = new_id { - self.previous_creation = Some(PreviousCreation { entity: id }); - } - } - Tool::CurrentSource => { - if previous_too_near { - return; - } - let id = world.spawn(( - Position { - position: mouse_position, - }, - StaticConnectionSource {}, - CurrentSource { current: 150.0 }, - Deletable {}, - Selectable { selected: false }, - )); - self.previous_creation = Some(PreviousCreation { entity: id }); - } - Tool::StaticConnection | Tool::LearningConnection => { - if let Some(ct) = connection_tool { - let nearest_target = world - .query::<&Position>() - .with::<&Neuron>() - .iter() - .min_by(|a, b| nearest(&mouse_position, a, b)) - .and_then(|v| within_attachment_range(mouse_position, v)); - if let Some((id, position)) = nearest_target { - let strength = if *tool == Tool::StaticConnection { - 1.0 - } else { - 0.0 - }; - let new_connection = Connection { - from: ct.from, - to: id, - strength, - directional: true, - }; - let connection_exists = - world.query::<&Connection>().iter().any(|(_, c)| { - c.from == new_connection.from && c.to == new_connection.to - }); - if !connection_exists && ct.from != id { - let synapse_current = SynapseCurrent { - current: 0.0, - tau: 0.1, - }; - if world.get::<&CurrentSource>(ct.from).is_ok() { - world.spawn((new_connection, Deletable {}, synapse_current)); - } else if world.get::<&Neuron>(ct.from).is_ok() { - if *tool == Tool::StaticConnection { - world.spawn((new_connection, Deletable {}, synapse_current)); - } else { - world.spawn(( - new_connection, - Deletable {}, - synapse_current, - LearningSynapse {}, - )); - }; - } - } - if !self.keyboard.shift_down { - ct.start = position; - ct.from = id; - } - } - ct.end = mouse_position; - } else { - *connection_tool = world - .query::<&Position>() - .with::<&StaticConnectionSource>() - .iter() - .min_by(|a, b| nearest(&mouse_position, a, b)) - .and_then(|v| within_attachment_range(mouse_position, v)) - .map(|(id, position)| ConnectionTool { - start: position, - end: mouse_position, - from: id, - }); - } - } - Tool::Stimulate => { - *stimulation_tool = Some(StimulationTool { - position: mouse_position, - }) - } - Tool::Erase => { - let to_delete = world - .query::<&Position>() - .with::<&Deletable>() - .iter() - .filter_map(|(entity, position)| { - let distance = position.position.distance(mouse_position); - if distance < NODE_RADIUS * 1.5 { - Some(entity) - } else { - None - } - }) - .collect::>(); - for entity in to_delete { - world.despawn(entity).unwrap(); - } - let connections_to_delete = world - .query::<&Connection>() - .with::<&Deletable>() - .iter() - .filter_map(|(entity, connection)| { - if let (Ok(from), Ok(to)) = ( - world.get::<&Position>(connection.from), - world.get::<&Position>(connection.to), - ) { - let a = from.position; - let b = to.position; - let p = mouse_position; - let ab = b - a; - let ap = p - a; - let t = ap.dot(ab) / ab.dot(ab); - let d = t * ab; - let point_on_line = a + d; - let distance_from_line = p.distance(point_on_line); - if distance_from_line < ERASE_RADIUS && (0.0..=1.0).contains(&t) { - Some(entity) - } else { - None - } - } else { - Some(entity) - } - }) - .collect::>(); - for connection in connections_to_delete { - world.despawn(connection).unwrap(); - } - let triggers_to_delete = world - .query::<&Trigger>() - .iter() - .filter_map(|(entity, trigger)| { - if world.get::<&Connection>(trigger.connection).is_err() { - Some(entity) - } else { - None - } - }) - .collect::>(); - for trigger in triggers_to_delete { - world.despawn(trigger).unwrap(); - } - } - Tool::Voltmeter => { - if previous_too_near { - return; - } - let result = world - .query::<&Position>() - .with::<&Neuron>() - .iter() - .find_map(|(entity, position)| { - let distance = position.position.distance(mouse_position); - if distance < NODE_RADIUS { - Some((entity, position.clone())) - } else { - None - } - }); - let Some((target, position)) = result else { - return; - }; - let voltmeter = world.spawn(( - Voltmeter {}, - Position { - position: position.position - + Vec3 { - x: 1.0, - y: 1.0, - z: 0.0, - }, - }, - )); - *previous_creation = Some(PreviousCreation { entity: voltmeter }); - world.spawn(( - VoltageSeries { - measurements: RollingWindow::new(100000), - }, - Connection { - from: target, - to: voltmeter, - strength: 1.0, - directional: true, - }, - )); - } - Tool::Select => match self.mouse.left_down { - true => match self.move_origin { - Some(origin) => { - let center = mouse_position - origin; - application.camera_controller.center -= - Vector3::new(center.x, center.y, center.z); - } - None => { - if let Some(entity) = world - .query::<&Position>() - .iter() - .min_by(|a, b| nearest(&mouse_position, a, b)) - .and_then(|v| within_selection_range(mouse_position, v)) - .map(|(id, _position)| id) - { - self.active_entity = Some(entity); - } else { - self.active_entity = None; - self.move_origin = Some(mouse_position); - } - } - }, - false => self.move_origin = None, - }, - Tool::Axon => match connection_tool { - None => { - *connection_tool = world - .query::<&Position>() - .with::<&StaticConnectionSource>() - .iter() - .min_by(|a, b| nearest(&mouse_position, a, b)) - .and_then(|v| within_attachment_range(mouse_position, v)) - .map(|(id, position)| ConnectionTool { - start: position, - end: mouse_position, - from: id, - }); - if let Some(ct) = connection_tool { - self.previous_creation = Some(PreviousCreation { entity: ct.from }); - } - } - Some(ct) => { - ct.end = mouse_position; - let nearest_target = world - .query::<&Position>() - .with::<&Neuron>() - .iter() - .min_by(|a, b| nearest(&mouse_position, a, b)) - .and_then(|v| within_attachment_range(mouse_position, v)); - - match nearest_target { - Some((id, position)) => { - let strength = 1.0; - let new_connection = Connection { - from: ct.from, - to: id, - strength, - directional: true, - }; - let synapse_current = SynapseCurrent { - current: 0.0, - tau: 0.1, - }; - let connection_exists = - world.query::<&Connection>().iter().any(|(_, c)| { - c.from == new_connection.from && c.to == new_connection.to - }); - if !connection_exists && ct.from != id { - world.spawn((new_connection, Deletable {}, synapse_current)); - } - if !self.keyboard.shift_down { - ct.start = position; - ct.from = id; - } - } - None => { - if previous_too_near { - return; - } - let neuron_type = - if let Ok(neuron_type) = world.get::<&NeuronType>(ct.from) { - Some((*neuron_type).clone()) - } else { - None - }; - if let Some(neuron_type) = neuron_type { - let compartment = world.spawn(( - Position { - position: mouse_position, - }, - neuron_type, - Compartment { - voltage: 100.0, - m: 0.084073044, - h: 0.45317015, - n: 0.38079754, - influence: 0.0, - capacitance: 4.0, - injected_current: 0.0, - }, - StaticConnectionSource {}, - Deletable {}, - Selectable { selected: false }, - SpatialDynamics { - velocity: Vec3::new(0.0, 0.0, 0.0), - acceleration: Vec3::new(0.0, 0.0, 0.0), - }, - )); - let strength = 1.0; - let new_connection = Connection { - from: ct.from, - to: compartment, - strength, - directional: false, - }; - world.spawn(( - new_connection, - Deletable {}, - CompartmentCurrent { capacitance: 1.0 }, - )); - self.previous_creation = Some(PreviousCreation { - entity: compartment, - }); - *connection_tool = Some(ConnectionTool { - start: mouse_position, - end: mouse_position, - from: compartment, - }); - } - } - } - } - }, - } - } - - pub fn save(&self, path: PathBuf) { - let mut context = SaveContext; - let mut serializer = postcard::Serializer { - output: postcard::ser_flavors::StdVec::new(), - }; - serialize(&self.world, &mut context, &mut serializer).unwrap(); - let mut writer = std::fs::File::create(path.with_extension("neuronify")).unwrap(); - writer - .write_all(&serializer.output.finalize().unwrap()) - .unwrap(); - } - - pub fn loadfile(&mut self, path: PathBuf) { - let mut context = LoadContext::new(); - let reader = std::fs::File::open(path).unwrap(); - let mut bufreader = BufReader::new(reader); - let mut bytes: Vec = Vec::new(); - bufreader.read_to_end(&mut bytes).unwrap(); - let mut deserializer = postcard::Deserializer::from_bytes(&bytes); - self.world = deserialize(&mut context, &mut deserializer).unwrap(); - } - - pub fn from_slice(application: &mut visula::Application, bytes: &[u8]) -> Neuronify { - let mut neuronify = Neuronify::new(application); - let mut context = LoadContext::new(); - let mut deserializer = postcard::Deserializer::from_bytes(bytes); - neuronify.world = deserialize(&mut context, &mut deserializer).unwrap(); - neuronify.edit_enabled = false; - neuronify - } -} - -fn srgb_component(value: u8) -> f32 { - (value as f32 / 255.0 + 0.055_f32).powf(2.44) / 1.055 -} - -fn srgb(red: u8, green: u8, blue: u8) -> Vec3 { - Vec3::new( - srgb_component(red), - srgb_component(green), - srgb_component(blue), - ) -} - -fn red() -> Vec3 { - srgb(210, 15, 57) -} - -fn blue() -> Vec3 { - srgb(30, 102, 245) -} - -fn base() -> Vec3 { - srgb(239, 241, 245) -} -fn mantle() -> Vec3 { - srgb(230, 233, 239) -} -fn crust() -> Vec3 { - srgb(220, 224, 232) -} -fn yellow() -> Vec3 { - srgb(223, 142, 29) -} -fn neurocolor(neuron_type: &NeuronType, value: f32) -> Vec3 { - let v = 1.0 / (1.0 + (-5.0 * (value - 0.5)).exp()); - match *neuron_type { - NeuronType::Excitatory => v * base() + (1.0 - v) * blue(), - NeuronType::Inhibitory => v * mantle() + (1.0 - v) * red(), - } -} - -impl visula::Simulation for Neuronify { - type Error = Error; - fn clear_color(&self) -> wgpu::Color { - wgpu::Color { - r: srgb_component(30) as f64, - g: srgb_component(30) as f64, - b: srgb_component(46) as f64, - a: 1.0, - } - } - fn update(&mut self, application: &mut visula::Application) { - let Neuronify { - connection_tool, - world, - time, - stimulation_tool, - .. - } = self; - let dt = 0.001; - let cdt = 0.01; - - for (_, (position, stimulate)) in world.query_mut::<(&Position, &mut StimulateCurrent)>() { - if let Some(stim) = stimulation_tool { - let mouse_distance = position.position.distance(stim.position); - stimulate.current = if mouse_distance < NODE_RADIUS { - 2000.0 - } else { - 0.0 - }; - } else { - stimulate.current = 0.0; - } - } - - for _ in 0..self.iterations { - for (_, dynamics) in world.query_mut::<&mut NeuronDynamics>() { - dynamics.current = 0.0; - } - for (_, (leak, neuron, dynamics)) in - world.query_mut::<(&mut LeakCurrent, &Neuron, &mut NeuronDynamics)>() - { - leak.current = (neuron.resting_potential - dynamics.voltage) / leak.tau; - dynamics.current += leak.current; - } - for (_, (dynamics, stimulate)) in - world.query_mut::<(&mut NeuronDynamics, &StimulateCurrent)>() - { - dynamics.current += stimulate.current; - } - for (_, synapse) in world.query_mut::<&mut SynapseCurrent>() { - synapse.current -= synapse.current * dt / synapse.tau; - } - - for (_, (synapse, connection)) in - world.query::<(&mut SynapseCurrent, &Connection)>().iter() - { - if let Ok(source) = world.get::<&CurrentSource>(connection.from) { - synapse.current = source.current; - } - if let (Ok(compartment), Ok(neuron_type)) = ( - world.get::<&Compartment>(connection.from), - world.get::<&NeuronType>(connection.from), - ) { - let current = (compartment.voltage - 50.0).clamp(0.0, 200.0); - let sign = match *neuron_type { - NeuronType::Excitatory => 1.0, - NeuronType::Inhibitory => -1.0, - }; - synapse.current += sign * current; - } - } - for (_, (synapse, connection)) in world.query::<(&SynapseCurrent, &Connection)>().iter() - { - let mut dynamics = world.get::<&mut NeuronDynamics>(connection.to).unwrap(); - if dynamics.refraction <= 0.0 { - dynamics.current += synapse.current; - } - } - for (_, (dynamics, neuron)) in world.query_mut::<(&mut NeuronDynamics, &Neuron)>() { - dynamics.voltage = (dynamics.voltage + dynamics.current * dt).clamp(-200.0, 200.0); - if dynamics.refraction <= 0.0 && dynamics.voltage > neuron.threshold { - dynamics.fired = true; - dynamics.refraction = 0.2; - dynamics.voltage = neuron.initial_voltage; - dynamics.voltage = neuron.reset_potential; - } - } - for (_, compartment) in world.query_mut::<&mut Compartment>() { - let v = compartment.voltage; - - let sodium_activation_alpha = 0.1 * (25.0 - v) / ((2.5 - 0.1 * v).exp() - 1.0); - let sodium_activation_beta = 4.0 * (-v / 18.0).exp(); - let sodium_inactivation_alpha = 0.07 * (-v / 20.0).exp(); - let sodium_inactivation_beta = 1.0 / ((3.0 - 0.1 * v).exp() + 1.0); - - let mut m = compartment.m; - let alpham = sodium_activation_alpha; - let betam = sodium_activation_beta; - let dm = cdt * (alpham * (1.0 - m) - betam * m); - let mut h = compartment.h; - let alphah = sodium_inactivation_alpha; - let betah = sodium_inactivation_beta; - let dh = cdt * (alphah * (1.0 - h) - betah * h); - - m += dm; - h += dh; - - m = m.clamp(0.0, 1.0); - h = h.clamp(0.0, 1.0); - - let g_na = 120.0; - - let ena = 115.0; - - let m3 = m * m * m; - - let sodium_current = -g_na * m3 * h * (compartment.voltage - ena); - - let potassium_activation_alpha = - 0.01 * (10.0 - v) / ((1.0 - (0.1 * v)).exp() - 1.0); - let potassium_activation_beta = 0.125 * (-v / 80.0).exp(); - - let mut n = compartment.n; - let alphan = potassium_activation_alpha; - let betan = potassium_activation_beta; - let dn = cdt * (alphan * (1.0 - n) - betan * n); - - n += dn; - n = n.clamp(0.0, 1.0); - - let g_k = 36.0; - let ek = -12.0; - let n4 = n * n * n * n; - - let potassium_current = -g_k * n4 * (compartment.voltage - ek); - - let e_m = 10.6; - let leak_conductance = 1.3; - let leak_current = -leak_conductance * (compartment.voltage - e_m); - - let current = sodium_current - + potassium_current - + leak_current - + compartment.injected_current; - let delta_voltage = current / compartment.capacitance; - - compartment.n = n; - compartment.m = m; - compartment.h = h; - compartment.voltage += delta_voltage * cdt; - compartment.voltage = compartment.voltage.clamp(-50.0, 200.0); - compartment.injected_current -= 1.0 * compartment.injected_current * cdt; - } - - let mut new_compartments: HashMap = world - .query::<&Compartment>() - .iter() - .map(|(entity, &compartment)| (entity, compartment)) - .collect(); - for (_, (connection, current)) in - world.query::<(&Connection, &CompartmentCurrent)>().iter() - { - if let Ok(compartment_to) = world.get::<&Compartment>(connection.to) { - if let Ok(dynamics_from) = world.get::<&NeuronDynamics>(connection.from) { - if dynamics_from.fired { - let new_compartment_to = new_compartments - .get_mut(&connection.to) - .expect("Could not get new compartment"); - new_compartment_to.injected_current += 150.0; - } - } else if let Ok(compartment_from) = world.get::<&Compartment>(connection.from) - { - let voltage_diff = compartment_from.voltage - compartment_to.voltage; - let delta_voltage = voltage_diff / current.capacitance; - let new_compartment_to = new_compartments - .get_mut(&connection.to) - .expect("Could not get new compartment"); - new_compartment_to.voltage += delta_voltage * cdt; - let new_compartment_from = new_compartments - .get_mut(&connection.from) - .expect("Could not get new compartment"); - new_compartment_from.voltage -= delta_voltage * cdt; - } - } - } - let positions: Vec<(Entity, Position)> = world - .query::<&Position>() - .iter() - .map(|(e, p)| (e.to_owned(), p.to_owned())) - .collect(); - for (id, (position, dynamics)) in world.query_mut::<(&Position, &mut SpatialDynamics)>() - { - for (other_id, other_position) in &positions { - if id == *other_id { - continue; - } - let from = position.position; - let to = other_position.position; - let r2 = from.distance_squared(to); - let target2 = (2.0 * NODE_RADIUS).powi(2); - let d = (to - from).normalize(); - let force = 5.0 * (r2 - target2).min(0.0) * d; - dynamics.acceleration += force; - } - } - let connections: Vec<(Entity, Connection)> = world - .query::<&Connection>() - .iter() - .map(|(e, c)| (e.to_owned(), c.to_owned())) - .collect(); - - for (connection_id_1, connection_1) in &connections { - for (connection_id_2, connection_2) in &connections { - if connection_id_1 == connection_id_2 { - continue; - } - if connection_1.to != connection_2.from { - continue; - } - let to_1 = world.get::<&Position>(connection_1.to).unwrap().position; - let from_1 = world.get::<&Position>(connection_1.from).unwrap().position; - let to_2 = world.get::<&Position>(connection_2.to).unwrap().position; - let from_2 = world.get::<&Position>(connection_2.from).unwrap().position; - let target = 1.0; - let dir_ab = (to_1 - from_1).normalize(); - let dir_bc = (to_2 - from_2).normalize(); - let dot = dir_ab.dot(dir_bc); - let diff = target - dot; - let p_a = (dir_ab.cross((dir_ab).cross(dir_bc))).normalize(); - let p_c = (dir_bc.cross((dir_ab).cross(dir_bc))).normalize(); - let k = 1.0; - let f_a = k * diff / dir_ab.length() * p_a; - let f_c = k * diff / dir_bc.length() * p_c; - let f_b = -f_a - f_c; - if f_a.is_nan() || f_b.is_nan() || f_c.is_nan() { - continue; - } - if let Ok(mut dynamics_a) = world.get::<&mut SpatialDynamics>(connection_1.from) - { - dynamics_a.acceleration += f_a; - } - if let Ok(mut dynamics_b) = world.get::<&mut SpatialDynamics>(connection_1.to) { - dynamics_b.acceleration += f_b; - } - if let Ok(mut dynamics_c) = world.get::<&mut SpatialDynamics>(connection_2.to) { - dynamics_c.acceleration += f_c; - } - } - } - - for (compartment_id, new_compartment) in new_compartments { - let mut old_compartment = world - .get::<&mut Compartment>(compartment_id) - .expect("Could not find compartment"); - *old_compartment = new_compartment; - } - let new_triggers: Vec<(Entity, f64)> = world - .query::<&Connection>() - .with::<&SynapseCurrent>() - .iter() - .flat_map(|(connection_entity, connection)| { - let mut triggers = vec![]; - if let (Ok(_neuron_from), Ok(neuron_from_type)) = ( - world.get::<&Neuron>(connection.from), - world.get::<&NeuronType>(connection.from), - ) { - if let Ok(dynamics_from) = world.get::<&NeuronDynamics>(connection.from) { - let base_current = match *neuron_from_type { - NeuronType::Excitatory => 3000.0, - NeuronType::Inhibitory => -3000.0, - }; - let current = connection.strength * base_current; - if dynamics_from.fired { - triggers.push((connection_entity, current)); - } - } - } - triggers - }) - .collect(); - for (connection_entity, current) in new_triggers { - world.spawn((Trigger::new(0.3, current, connection_entity),)); - } - - for (_, connection) in world - .query::<&Connection>() - .with::<&CompartmentCurrent>() - .iter() - { - if let (Ok(from), Ok(to)) = ( - world.get::<&Position>(connection.from), - world.get::<&Position>(connection.to), - ) { - let r2 = from.position.distance_squared(to.position); - let d = to.position - from.position; - let target_length = 2.0 * NODE_RADIUS; - let force = 10.0 * (r2 - target_length.powi(2)) * d.normalize(); - if let Ok(mut dynamics_from) = - world.get::<&mut SpatialDynamics>(connection.from) - { - dynamics_from.acceleration += force; - } - if let Ok(mut dynamics_to) = world.get::<&mut SpatialDynamics>(connection.to) { - dynamics_to.acceleration -= force; - } - } - } - - for (_, trigger) in world.query_mut::<&mut Trigger>() { - trigger.decrement(dt); - } - - let triggers_to_delete: Vec = world - .query::<&Trigger>() - .iter() - .filter_map(|(entity, trigger)| { - if trigger.done() { - let mut synapse = world - .get::<&mut SynapseCurrent>(trigger.connection) - .unwrap(); - synapse.current = trigger.current; - Some(entity) - } else { - None - } - }) - .collect(); - - for entity in triggers_to_delete { - world.despawn(entity).expect("Could not delete entity!"); - } - - for (_, dynamics) in world.query_mut::<&mut NeuronDynamics>() { - dynamics.fired = false; - dynamics.refraction -= dt; - } - - for (_, (position, dynamics)) in - world.query_mut::<(&mut Position, &mut SpatialDynamics)>() - { - let gravity = -position.position.y; - dynamics.acceleration += Vec3::new(0.0, gravity, 0.0); - dynamics.velocity += dynamics.acceleration * dt as f32; - position.position += dynamics.velocity * dt as f32; - dynamics.acceleration = Vec3::new(0.0, 0.0, 0.0); - dynamics.velocity -= dynamics.velocity * dt as f32; - } - - let mut updates = HashMap::new(); - for (entity, (_, connection)) in world.query::<(&VoltageSeries, &Connection)>().iter() { - let dynamics = world - .get::<&NeuronDynamics>(connection.from) - .expect("Connection with voltage series does not come from neuron"); - updates.insert( - entity, - VoltageMeasurement { - voltage: dynamics.voltage, - time: *time, - }, - ); - } - for (entity, value) in updates { - world - .get::<&mut VoltageSeries>(entity) - .unwrap() - .measurements - .push(value); - } - - *time += dt; - } - - let neuron_spheres: Vec = world - .query::<(&Neuron, &NeuronDynamics, &Position, &NeuronType)>() - .iter() - .map(|(_entity, (neuron, dynamics, position, neuron_type))| { - let value = ((dynamics.voltage - neuron.resting_potential) - / (neuron.threshold - neuron.resting_potential)) - .clamp(0.0, 1.0) as f32; - Sphere { - position: position.position, - color: neurocolor(neuron_type, value), - radius: NODE_RADIUS, - _padding: Default::default(), - } - }) - .collect(); - - let compartment_spheres: Vec = world - .query::<(&Compartment, &Position, &NeuronType)>() - .iter() - .map(|(_entity, (compartment, position, neuron_type))| { - let value = ((compartment.voltage + 10.0) / 120.0) as f32; - let _color = match neuron_type { - NeuronType::Excitatory => Vec3::new(value / 2.0, value, 0.95), - NeuronType::Inhibitory => Vec3::new(0.95, value / 2.0, value), - }; - Sphere { - position: position.position, - color: neurocolor(neuron_type, value), - radius: 0.3 * NODE_RADIUS, - _padding: Default::default(), - } - }) - .collect(); - - let source_spheres: Vec = world - .query::<&Position>() - .with::<&CurrentSource>() - .iter() - .map(|(_entity, position)| Sphere { - position: position.position, - color: yellow(), - radius: NODE_RADIUS, - _padding: Default::default(), - }) - .collect(); - - let trigger_spheres: Vec = world - .query::<&Trigger>() - .iter() - .map(|(_entity, trigger)| { - let connection = world - .get::<&Connection>(trigger.connection) - .expect("Connection from broken"); - let start = world - .get::<&Position>(connection.from) - .expect("Connection from broken") - .position; - let end = world - .get::<&Position>(connection.to) - .expect("Connection to broken") - .position; - let diff = end - start; - let position = start + diff * trigger.progress() as f32; - Sphere { - position, - color: crust(), - radius: NODE_RADIUS * 0.5, - _padding: Default::default(), - } - }) - .collect(); - - let mut spheres = Vec::new(); - spheres.extend(neuron_spheres.iter()); - spheres.extend(compartment_spheres.iter()); - spheres.extend(source_spheres.iter()); - spheres.extend(trigger_spheres.iter()); - - let mut connections: Vec = world - .query::<&Connection>() - .iter() - .map(|(_, connection)| { - let start = world - .get::<&Position>(connection.from) - .expect("Connection from broken") - .position; - let end = world - .get::<&Position>(connection.to) - .expect("Connection to broken") - .position; - let value = |target: Entity| -> f32 { - if let Ok(compartment) = world.get::<&Compartment>(target) { - ((compartment.voltage + 10.0) / 120.0) as f32 - } else if let Ok(neuron_dynamics) = world.get::<&NeuronDynamics>(target) { - ((neuron_dynamics.voltage + 10.0) / 120.0) as f32 - } else { - 1.0 - } - }; - let start_value = value(connection.to); - let end_value = value(connection.from); - let (start_color, end_color) = - if world.get::<&CurrentSource>(connection.from).is_ok() { - (yellow(), yellow()) - } else if let Ok(neuron_type) = world.get::<&NeuronType>(connection.from) { - ( - neurocolor(&neuron_type, start_value), - neurocolor(&neuron_type, end_value), - ) - } else { - (crust(), crust()) - }; - ConnectionData { - position_a: start, - position_b: end, - strength: connection.strength as f32, - directional: match connection.directional { - true => 1.0, - false => 0.0, - }, - start_color, - end_color, - _padding: Default::default(), - } - }) - .collect(); - - if self.tool == Tool::StaticConnection || self.tool == Tool::LearningConnection { - if let Some(connection) = &connection_tool { - connections.push(ConnectionData { - position_a: connection.start, - position_b: connection.end, - strength: 1.0, - directional: 1.0, - start_color: Vec3::new(0.8, 0.8, 0.8), - end_color: Vec3::new(0.8, 0.8, 0.8), - _padding: Default::default(), - }); - } - } - - self.sphere_buffer - .update(&application.device, &application.queue, &spheres); - - self.connection_buffer - .update(&application.device, &application.queue, &connections); - - let time_diff = Utc::now() - self.last_update; - #[cfg(not(target_arch = "wasm32"))] - if time_diff < Duration::milliseconds(16) { - thread::sleep(std::time::Duration::from_millis( - (Duration::milliseconds(16) - time_diff).num_milliseconds() as u64, - )) - } - let low_pass_factor = 0.05; - let new_fps = 1.0 - / ((Utc::now() - self.last_update).num_nanoseconds().unwrap() as f64 * 1e-9) - .max(0.0000001); - self.fps = (1.0 - low_pass_factor) * self.fps + low_pass_factor * new_fps; - self.last_update = Utc::now(); - } - - fn render(&mut self, data: &mut RenderData) { - self.spheres.render(data); - self.connection_lines.render(data); - self.connection_spheres.render(data); - } - - fn gui(&mut self, application: &visula::Application, context: &egui::Context) { - egui::Area::new("edit_button_area") - .anchor(egui::Align2::RIGHT_BOTTOM, [-10.0, -10.0]) - .show(context, |ui| { - ui.toggle_value(&mut self.edit_enabled, "Edit").clicked(); - }); - if self.edit_enabled { - #[cfg(not(target_arch = "wasm32"))] - egui::TopBottomPanel::top("top_panel").show(context, |ui| { - egui::menu::bar(ui, |ui| { - ui.menu_button("File", |ui| { - if ui.button("Save").clicked() { - if let Some(path) = rfd::FileDialog::new().save_file() { - self.save(path); - } - } - - if ui.button("Open").clicked() { - if let Some(path) = rfd::FileDialog::new().pick_file() { - self.loadfile(path); - } - } - }); - }); - }); - egui::Window::new("Settings").show(context, |ui| { - ui.label(format!("FPS: {:.0}", self.fps)); - ui.label("Tool"); - for value in Tool::iter() { - ui.selectable_value(&mut self.tool, value.clone(), format!("{:?}", &value)); - } - ui.label("Simulation speed"); - ui.add(egui::Slider::new(&mut self.iterations, 1..=20)); - }); - if let Some(active_entity) = self.active_entity { - egui::Window::new("Selection").show(context, |ui| { - if let Ok(compartment) = self.world.get::<&Compartment>(active_entity) { - ui.collapsing("Compartment", |ui| { - egui::Grid::new("compartment_state").show(ui, |ui| { - ui.label("Voltage:"); - ui.label(format!("{:.2} mV", compartment.voltage)); - ui.end_row(); - - ui.label("m:"); - ui.label(format!("{:.2}", compartment.m)); - ui.end_row(); - - ui.label("n:"); - ui.label(format!("{:.2}", compartment.n)); - ui.end_row(); - - ui.label("h:"); - ui.label(format!("{:.2}", compartment.h)); - ui.end_row(); - }); - }); - } - if let Ok(mut neuron) = self.world.get::<&mut Neuron>(active_entity) { - ui.collapsing("Neuron", |ui| { - egui::Grid::new("neuron_settings").show(ui, |ui| { - ui.label("Threshold:"); - ui.add(egui::Slider::new(&mut neuron.threshold, -10.0..=100.0)); - ui.end_row(); - - ui.label("Resting potential:"); - ui.add(egui::Slider::new( - &mut neuron.resting_potential, - -120.0..=-40.0, - )); - ui.end_row(); - }); - }); - } - if let Ok(dynamics) = self.world.get::<&NeuronDynamics>(active_entity) { - ui.collapsing("Dynamics", |ui| { - egui::Grid::new("neuron_dynamics").show(ui, |ui| { - ui.label("Voltage:"); - ui.label(format!("{:.2} mV", dynamics.voltage)); - ui.end_row(); - }); - }); - } - if let Ok(mut leak_current) = self.world.get::<&mut LeakCurrent>(active_entity) - { - ui.collapsing("Leak current", |ui| { - egui::Grid::new("neuron_settings").show(ui, |ui| { - ui.label("Tau:"); - ui.add(egui::Slider::new(&mut leak_current.tau, 0.01..=10.0)); - ui.end_row(); - }); - }); - } - }); - } - } - - for (voltmeter_id, _voltmeter) in self.world.query::<&Voltmeter>().iter() { - for (_, (series, connection)) in - self.world.query::<(&VoltageSeries, &Connection)>().iter() - { - if connection.to != voltmeter_id { - continue; - } - let Ok(position) = self.world.get::<&Position>(connection.from) else { - log::error!("Position not found for entity"); - continue; - }; - let id = egui::Id::new(voltmeter_id); - egui::Window::new("Voltmeter") - .id(id) - .resizable(true) - .show(context, |ui| { - let line_points: PlotPoints = series - .measurements - .iter() - .map(|m| [m.time, m.voltage]) - .collect(); - let (min_x, max_x) = { - match series.measurements.last().map(|m| m.time) { - Some(t) => (t - 5.0, t), - None => (-5.0, 0.0), - } - }; - let line = Line::new(line_points); - egui_plot::Plot::new("Voltage") - .show(ui, |plot_ui| { - plot_ui.set_plot_bounds(PlotBounds::from_min_max( - [min_x, -100.0], - [max_x, 100.0], - )); - plot_ui.line(line) - }) - .response - }); - - let mut start = Pos2::new(0.0, 0.0); - context.memory(|memory| { - let rect = memory - .area_rect(id) - .expect("Could not find id of window that was just created"); - start = rect.center(); - }); - let width = application.config.width as f32; - let height = application.config.height as f32; - let position_2d_pre = application - .camera_controller - .uniforms(width / height) - .model_view_projection_matrix - * Vector4::new( - position.position.x, - position.position.y, - position.position.z, - 1.0, - ); - - let position_2d = position_2d_pre / position_2d_pre.w; - - let line_end = ( - width / application.window.scale_factor() as f32 * (position_2d[0] + 1.0) / 2.0, - height / application.window.scale_factor() as f32 - * (((0.0 - position_2d[1]) + 1.0) / 2.0), - ) - .into(); - context - .layer_painter(LayerId::background()) - .line_segment([start, line_end], (1.0, Color32::WHITE)); // Adjust color and line thickness as needed - } - } - } - - fn handle_event(&mut self, application: &mut visula::Application, event: &Event) { - if let Event::WindowEvent { window_id, .. } = event { - if &application.window.id() != window_id { - return; - } - } - match event { - Event::WindowEvent { - event: - WindowEvent::MouseInput { - state, - button: MouseButton::Left, - .. - }, - .. - } => { - self.mouse.left_down = *state == ElementState::Pressed; - self.mouse.delta_position = None; - self.handle_tool(application); - } - Event::WindowEvent { - event: WindowEvent::ModifiersChanged(state), - .. - } => { - self.keyboard.shift_down = state.lshift_state() == ModifiersKeyState::Pressed - || state.rshift_state() == ModifiersKeyState::Pressed; - } - Event::WindowEvent { - event: WindowEvent::CursorMoved { position, .. }, - .. - } => { - self.mouse.delta_position = self.mouse.position.map(|previous| { - PhysicalPosition::new(position.x - previous.x, position.y - previous.y) - }); - self.mouse.position = Some(*position); - self.handle_tool(application); - } - _ => {} - } - } -} +pub use app::{Error, Neuronify}; +pub use components::*; +pub use constants::*; +pub use input::{Keyboard, Mouse}; +pub use simulation::{fhn_step, lif_step, run_headless, SpikeRecord}; +pub use tools::*; +#[cfg(target_arch = "wasm32")] struct Bundle { application: Application, simulation: Neuronify, } +#[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub struct WasmWrapper { event_loop: EventLoop, bundles: Vec, } +#[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub async fn initialize() -> WasmWrapper { initialize_logger(); @@ -1713,15 +62,16 @@ pub async fn initialize() -> WasmWrapper { } } +#[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub async fn load(wrapper: &mut WasmWrapper, canvas: &str, url: &str) -> Result<(), JsValue> { - let window = create_window( - RunConfig { + let window = visula::create_window_with_config( + &RunConfig { canvas_name: canvas.to_owned(), }, - &wrapper.event_loop, + todo!("Need ActiveEventLoop from ApplicationHandler"), ); - let mut application = pollster::block_on(async { Application::new(Arc::new(window)).await }); + let mut application = Application::new(window).await; let mut opts = RequestInit::new(); opts.method("GET"); @@ -1740,32 +90,9 @@ pub async fn load(wrapper: &mut WasmWrapper, canvas: &str, url: &str) -> Result< Ok(()) } +#[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub async fn start(mut wrapper: WasmWrapper) -> Result<(), JsValue> { - let _event_handler = move |event, target: &EventLoopWindowTarget| { - for bundle in wrapper.bundles.iter_mut() { - let application = &mut bundle.application; - let simulation = &mut bundle.simulation; - if !application.handle_event(&event) { - simulation.handle_event(application, &event); - } - if let Event::WindowEvent { ref event, .. } = event { - match event { - WindowEvent::RedrawRequested => { - application.update(); - simulation.update(application); - application.render(simulation); - - application.window.borrow_mut().request_redraw(); - } - WindowEvent::CloseRequested => target.exit(), - _ => {} - } - } - } - }; - #[cfg(target_arch = "wasm32")] - wrapper.event_loop.spawn(_event_handler); Ok(()) } diff --git a/neuronify-core/src/measurement/voltmeter.rs b/neuronify-core/src/measurement/voltmeter.rs index 1afdbd78..677746e5 100644 --- a/neuronify-core/src/measurement/voltmeter.rs +++ b/neuronify-core/src/measurement/voltmeter.rs @@ -38,6 +38,8 @@ pub struct VoltageMeasurement { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct VoltageSeries { pub measurements: RollingWindow, + #[serde(default)] + pub spike_times: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/neuronify-core/src/rendering/colors.rs b/neuronify-core/src/rendering/colors.rs new file mode 100644 index 00000000..122a932c --- /dev/null +++ b/neuronify-core/src/rendering/colors.rs @@ -0,0 +1,59 @@ +use glam::Vec3; + +use crate::components::NeuronType; + +pub fn srgb_component(value: u8) -> f32 { + (value as f32 / 255.0 + 0.055_f32).powf(2.44) / 1.055 +} + +pub fn srgb(red: u8, green: u8, blue: u8) -> Vec3 { + Vec3::new( + srgb_component(red), + srgb_component(green), + srgb_component(blue), + ) +} + +pub fn red() -> Vec3 { + srgb(210, 15, 57) +} + +pub fn blue() -> Vec3 { + srgb(30, 102, 245) +} + +pub fn base() -> Vec3 { + srgb(239, 241, 245) +} + +pub fn mantle() -> Vec3 { + srgb(230, 233, 239) +} + +pub fn crust() -> Vec3 { + srgb(220, 224, 232) +} + +pub fn yellow() -> Vec3 { + srgb(223, 142, 29) +} + +pub fn orange() -> Vec3 { + srgb(254, 100, 11) +} + +pub fn green() -> Vec3 { + srgb(64, 160, 43) +} + +pub fn frame_color() -> Vec3 { + srgb(80, 80, 100) +} + +pub fn neurocolor(neuron_type: &NeuronType, value: f32) -> Vec3 { + let v = 1.0 / (1.0 + (-5.0 * (value - 0.5)).exp()); + match *neuron_type { + NeuronType::Excitatory => v * base() + (1.0 - v) * blue(), + NeuronType::Inhibitory => v * mantle() + (1.0 - v) * red(), + } +} diff --git a/neuronify-core/src/rendering/connections.rs b/neuronify-core/src/rendering/connections.rs new file mode 100644 index 00000000..338946fb --- /dev/null +++ b/neuronify-core/src/rendering/connections.rs @@ -0,0 +1,143 @@ +use std::collections::HashSet; + +use glam::Vec3; +use hecs::Entity; + +use crate::components::*; +use crate::constants::*; +use crate::measurement::voltmeter::Voltmeter; +use crate::rendering::colors::*; +use crate::rendering::gpu_types::ConnectionData; +use crate::ConnectionTool; +use crate::Tool; + +fn quadratic_bezier(p0: Vec3, p1: Vec3, p2: Vec3, t: f32) -> Vec3 { + let u = 1.0 - t; + u * u * p0 + 2.0 * u * t * p1 + t * t * p2 +} + +pub fn collect_connections( + world: &hecs::World, + tool: &Tool, + connection_tool: &Option, +) -> Vec { + let connection_info: Vec<(Entity, Entity, Entity, f32, bool)> = world + .query::<&Connection>() + .iter() + .map(|(e, c)| { + let is_voltmeter = world.get::<&Voltmeter>(e).is_ok(); + ( + e, + c.from, + c.to, + c.strength as f32, + if is_voltmeter { false } else { c.directional }, + ) + }) + .collect(); + + let connection_pairs: HashSet<(Entity, Entity)> = connection_info + .iter() + .map(|(_, from, to, _, _)| (*from, *to)) + .collect(); + + let mut connections: Vec = Vec::new(); + + for &(_edge_entity, from, to, strength, directional) in &connection_info { + let start = world + .get::<&Position>(from) + .expect("Connection from broken") + .position; + let end = world + .get::<&Position>(to) + .expect("Connection to broken") + .position; + let value = |target: Entity| -> f32 { + if let Ok(compartment) = world.get::<&Compartment>(target) { + ((compartment.voltage + 50.0) / 200.0) as f32 + } else if let Ok(dynamics) = world.get::<&LeakyDynamics>(target) { + let neuron = world.get::<&LeakyNeuron>(target).ok(); + if let Some(neuron) = neuron { + ((dynamics.voltage - neuron.resting_potential) + / (neuron.threshold - neuron.resting_potential)) + .clamp(0.0, 1.0) as f32 + } else { + 0.5 + } + } else { + 1.0 + } + }; + let start_value = value(to); + let end_value = value(from); + let (start_color, end_color) = if world.get::<&CurrentClamp>(from).is_ok() { + (yellow(), yellow()) + } else if world.get::<&GeneratorDynamics>(from).is_ok() { + (orange(), orange()) + } else if let Ok(neuron_type) = world.get::<&NeuronType>(from) { + ( + neurocolor(&neuron_type, start_value), + neurocolor(&neuron_type, end_value), + ) + } else { + (crust(), crust()) + }; + + let is_reciprocal = connection_pairs.contains(&(to, from)); + let dir_val = if directional { 1.0 } else { 0.0 }; + + if is_reciprocal { + let segments = BEZIER_SEGMENTS; + let diff = end - start; + let up = Vec3::new(0.0, 1.0, 0.0); + let right = diff.cross(up); + let bend_amount = BEZIER_BEND_FRACTION * diff.length(); + let control = (start + end) * 0.5 + right.normalize_or_zero() * bend_amount; + + for i in 0..segments { + let t0 = i as f32 / segments as f32; + let t1 = (i + 1) as f32 / segments as f32; + let p0 = quadratic_bezier(start, control, end, t0); + let p1 = quadratic_bezier(start, control, end, t1); + let c0 = start_color.lerp(end_color, t0); + let c1 = start_color.lerp(end_color, t1); + let is_last = i == segments - 1; + connections.push(ConnectionData { + position_a: p0, + position_b: p1, + strength, + directional: if is_last { dir_val } else { 0.0 }, + start_color: c0, + end_color: c1, + _padding: Default::default(), + }); + } + } else { + connections.push(ConnectionData { + position_a: start, + position_b: end, + strength, + directional: dir_val, + start_color, + end_color, + _padding: Default::default(), + }); + } + } + + if *tool == Tool::StaticConnection { + if let Some(connection) = connection_tool { + connections.push(ConnectionData { + position_a: connection.start, + position_b: connection.end, + strength: 1.0, + directional: 1.0, + start_color: Vec3::new(0.8, 0.8, 0.8), + end_color: Vec3::new(0.8, 0.8, 0.8), + _padding: Default::default(), + }); + } + } + + connections +} diff --git a/neuronify-core/src/rendering/gpu_types.rs b/neuronify-core/src/rendering/gpu_types.rs new file mode 100644 index 00000000..a225c535 --- /dev/null +++ b/neuronify-core/src/rendering/gpu_types.rs @@ -0,0 +1,40 @@ +use bytemuck::{Pod, Zeroable}; +use glam::{Quat, Vec3}; +use visula_derive::Instance; + +#[repr(C, align(16))] +#[derive(Clone, Copy, Debug, Instance, Pod, Zeroable)] +pub struct Sphere { + pub position: Vec3, + pub color: Vec3, + pub radius: f32, + pub _padding: f32, +} + +#[repr(C, align(16))] +#[derive(Clone, Copy, Instance, Pod, Zeroable)] +pub struct ConnectionData { + pub start_color: Vec3, + pub end_color: Vec3, + pub position_a: Vec3, + pub position_b: Vec3, + pub strength: f32, + pub directional: f32, + pub _padding: [f32; 2], +} + +#[repr(C, align(16))] +#[derive(Clone, Copy, Instance, Pod, Zeroable)] +pub struct LineData { + pub start: Vec3, + pub end: Vec3, + pub _padding: [f32; 2], +} + +#[repr(C, align(16))] +#[derive(Clone, Copy, Instance, Pod, Zeroable)] +pub struct MeshInstanceData { + pub position: Vec3, + pub _padding: f32, + pub rotation: Quat, +} diff --git a/neuronify-core/src/rendering/mod.rs b/neuronify-core/src/rendering/mod.rs new file mode 100644 index 00000000..3a0dec52 --- /dev/null +++ b/neuronify-core/src/rendering/mod.rs @@ -0,0 +1,11 @@ +pub mod colors; +pub mod connections; +pub mod gpu_types; +pub mod spheres; +pub mod voltmeter; + +pub use colors::{neurocolor, srgb, srgb_component}; +pub use connections::collect_connections; +pub use gpu_types::{ConnectionData, LineData, MeshInstanceData, Sphere}; +pub use spheres::collect_spheres; +pub use voltmeter::collect_voltmeter_traces; diff --git a/neuronify-core/src/rendering/spheres.rs b/neuronify-core/src/rendering/spheres.rs new file mode 100644 index 00000000..272185bf --- /dev/null +++ b/neuronify-core/src/rendering/spheres.rs @@ -0,0 +1,111 @@ +use glam::Vec3; + +use crate::components::*; +use crate::constants::*; +use crate::rendering::colors::*; +use crate::rendering::gpu_types::Sphere; + +pub fn collect_spheres(world: &hecs::World) -> Vec { + let mut spheres = Vec::new(); + + let lif_neuron_spheres: Vec = world + .query::<(&LeakyNeuron, &LeakyDynamics, &Position)>() + .iter() + .map(|(entity, (neuron, dynamics, position))| { + let value = ((dynamics.voltage - neuron.resting_potential) + / (neuron.threshold - neuron.resting_potential)) + .clamp(0.0, 1.0) as f32; + let is_inhibitory = world.get::<&Inhibitory>(entity).is_ok(); + let color = if is_inhibitory { + value * mantle() + (1.0 - value) * red() + } else { + value * base() + (1.0 - value) * blue() + }; + Sphere { + position: position.position, + color, + radius: NODE_RADIUS, + _padding: Default::default(), + } + }) + .collect(); + + let current_clamp_spheres: Vec = world + .query::<&Position>() + .with::<&CurrentClamp>() + .iter() + .map(|(_, position)| Sphere { + position: position.position, + color: yellow(), + radius: NODE_RADIUS, + _padding: Default::default(), + }) + .collect(); + + let generator_spheres: Vec = world + .query::<(&Position, &GeneratorDynamics)>() + .iter() + .map(|(_, (position, _))| Sphere { + position: position.position, + color: orange(), + radius: NODE_RADIUS, + _padding: Default::default(), + }) + .collect(); + + let compartment_spheres: Vec = world + .query::<(&Compartment, &Position, &NeuronType)>() + .iter() + .map(|(_, (compartment, position, neuron_type))| { + let value = ((compartment.voltage + 50.0) / 200.0) as f32; + Sphere { + position: position.position, + color: neurocolor(neuron_type, value), + radius: COMPARTMENT_SPHERE_SCALE * NODE_RADIUS, + _padding: Default::default(), + } + }) + .collect(); + + let trigger_spheres: Vec = world + .query::<(&CurrentSynapse, &Connection)>() + .iter() + .flat_map(|(_, (synapse, connection))| { + let start = world + .get::<&Position>(connection.from) + .map(|p| p.position) + .unwrap_or(Vec3::ZERO); + let end = world + .get::<&Position>(connection.to) + .map(|p| p.position) + .unwrap_or(Vec3::ZERO); + let diff = end - start; + synapse + .triggers + .iter() + .map(move |&trigger_time| { + let fire_time = trigger_time - synapse.delay; + let progress = if synapse.delay > 0.0 { + ((synapse.time - fire_time) / synapse.delay).clamp(0.0, 1.0) as f32 + } else { + 1.0 + }; + Sphere { + position: start + diff * progress, + color: crust(), + radius: NODE_RADIUS * TRIGGER_SPHERE_SCALE, + _padding: Default::default(), + } + }) + .collect::>() + }) + .collect(); + + spheres.extend(lif_neuron_spheres.iter()); + spheres.extend(current_clamp_spheres.iter()); + spheres.extend(generator_spheres.iter()); + spheres.extend(compartment_spheres.iter()); + spheres.extend(trigger_spheres.iter()); + + spheres +} diff --git a/neuronify-core/src/rendering/voltmeter.rs b/neuronify-core/src/rendering/voltmeter.rs new file mode 100644 index 00000000..ea456d3e --- /dev/null +++ b/neuronify-core/src/rendering/voltmeter.rs @@ -0,0 +1,123 @@ +use glam::Vec3; + +use crate::components::*; +use crate::constants::*; +use crate::measurement::voltmeter::{VoltageSeries, Voltmeter}; +use crate::rendering::colors; +use crate::rendering::gpu_types::ConnectionData; + +pub fn collect_voltmeter_traces(world: &hecs::World) -> Vec { + let mut result = Vec::new(); + + for (voltmeter_id, _) in world.query::<&Voltmeter>().iter() { + let (series, spike_times, voltmeter_pos, trace_width, trace_height, is_compartment) = { + let Ok(series) = world.get::<&VoltageSeries>(voltmeter_id) else { + continue; + }; + let Ok(pos) = world.get::<&Position>(voltmeter_id) else { + continue; + }; + let size = world.get::<&VoltmeterSize>(voltmeter_id).ok(); + let tw = size.as_ref().map(|s| s.width).unwrap_or(8.0); + let th = size.as_ref().map(|s| s.height).unwrap_or(4.0); + let is_comp = world + .get::<&Connection>(voltmeter_id) + .ok() + .map(|conn| world.get::<&Compartment>(conn.from).is_ok()) + .unwrap_or(false); + let measurements: Vec<_> = series + .measurements + .iter() + .map(|m| (m.time, m.voltage)) + .collect(); + let spikes = series.spike_times.clone(); + let vpos = pos.position; + (measurements, spikes, vpos, tw, th, is_comp) + }; + + if series.len() < 2 { + continue; + } + + let time_window = VOLTMETER_TIME_WINDOW; + let (v_min, v_max) = if is_compartment { + (FHN_VOLTAGE_MIN, FHN_VOLTAGE_MAX) + } else { + (LIF_VOLTAGE_MIN, LIF_VOLTAGE_MAX) + }; + + let latest_time = series.last().map(|(t, _)| *t).unwrap_or(0.0); + let start_time = latest_time - time_window; + + let bottom_left_origin = voltmeter_pos + Vec3::new(-trace_height * 0.5, 0.0, 0.0); + + let trace_color = colors::green(); + let fc = colors::frame_color(); + + let bottom_left = bottom_left_origin; + let bottom_right = bottom_left_origin + Vec3::new(0.0, 0.0, trace_width); + let top_left = bottom_left_origin + Vec3::new(trace_height, 0.0, 0.0); + let top_right = bottom_left_origin + Vec3::new(trace_height, 0.0, trace_width); + for (a, b) in [ + (top_left, top_right), + (top_right, bottom_right), + (bottom_right, bottom_left), + (bottom_left, top_left), + ] { + result.push(ConnectionData { + position_a: a, + position_b: b, + strength: 0.3, + directional: 0.0, + start_color: fc, + end_color: fc, + _padding: Default::default(), + }); + } + + let visible: Vec<_> = series.iter().filter(|(t, _)| *t >= start_time).collect(); + + for window in visible.windows(2) { + let (t0, v0) = window[0]; + let (t1, v1) = window[1]; + + let z0 = ((t0 - start_time) / time_window) as f32 * trace_width; + let z1 = ((t1 - start_time) / time_window) as f32 * trace_width; + let x0 = ((v0 - v_min) / (v_max - v_min)) as f32 * trace_height; + let x1 = ((v1 - v_min) / (v_max - v_min)) as f32 * trace_height; + + let p0 = bottom_left_origin + Vec3::new(x0, 0.0, z0); + let p1 = bottom_left_origin + Vec3::new(x1, 0.0, z1); + + result.push(ConnectionData { + position_a: p0, + position_b: p1, + strength: 0.3, + directional: 0.0, + start_color: trace_color, + end_color: trace_color, + _padding: Default::default(), + }); + } + + for spike_time in &spike_times { + if *spike_time < start_time || *spike_time > latest_time { + continue; + } + let z = ((spike_time - start_time) / time_window) as f32 * trace_width; + let top = bottom_left_origin + Vec3::new(trace_height, 0.0, z); + let bottom = bottom_left_origin + Vec3::new(0.0, 0.0, z); + result.push(ConnectionData { + position_a: top, + position_b: bottom, + strength: 0.3, + directional: 0.0, + start_color: trace_color, + end_color: trace_color, + _padding: Default::default(), + }); + } + } + + result +} diff --git a/neuronify-core/src/serialization.rs b/neuronify-core/src/serialization.rs index da447da5..06e95289 100644 --- a/neuronify-core/src/serialization.rs +++ b/neuronify-core/src/serialization.rs @@ -1,8 +1,5 @@ -use crate::{ - Compartment, CompartmentCurrent, Connection, CurrentSource, Deletable, LeakCurrent, - LearningSynapse, Neuron, NeuronDynamics, NeuronType, Position, Selectable, SpatialDynamics, - StaticConnectionSource, StimulateCurrent, SynapseCurrent, Trigger, VoltageSeries, Voltmeter, -}; +use crate::components::*; +use crate::measurement::voltmeter::{VoltageSeries, Voltmeter}; use hecs::{serialize::column::*, *}; use serde::{Deserialize, Serialize}; use std::any::TypeId; @@ -107,22 +104,28 @@ macro_rules! component_id { component_id!( Position, - Neuron, - CurrentSource, - StaticConnectionSource, - NeuronDynamics, + LeakyNeuron, + LeakyDynamics, LeakCurrent, + AdaptationCurrent, + CurrentClamp, + CurrentSynapse, + ImmediateFireSynapse, + Inhibitory, + TouchSensor, + GeneratorDynamics, + RegularSpikeGenerator, + PoissonGenerator, + VoltmeterSize, + Annotation, + StaticConnectionSource, Deletable, Selectable, - LearningSynapse, - SynapseCurrent, Voltmeter, VoltageSeries, Connection, - Trigger, CompartmentCurrent, Compartment, SpatialDynamics, - StimulateCurrent, NeuronType, ); diff --git a/neuronify-core/src/simulation/fhn.rs b/neuronify-core/src/simulation/fhn.rs new file mode 100644 index 00000000..a05fb80d --- /dev/null +++ b/neuronify-core/src/simulation/fhn.rs @@ -0,0 +1,227 @@ +use std::collections::{HashMap, HashSet}; + +use crate::components::*; +use crate::constants::*; + +pub fn fhn_step(world: &mut hecs::World, cdt: f64, recently_fired: &HashSet) { + for (_, compartment) in world.query_mut::<&mut Compartment>() { + let v = (compartment.voltage - FHN_OFFSET) / FHN_SCALE; + let w = compartment.m; + let dv = FHN_TAU * (v - v * v * v / 3.0 - w); + let dw = FHN_TAU * FHN_EPS * (v + FHN_A - FHN_B * w); + let new_v = v + dv * cdt; + let new_w = w + dw * cdt; + compartment.voltage = new_v * FHN_SCALE + FHN_OFFSET; + compartment.m = new_w; + } + + let mut new_compartments: HashMap = world + .query::<&Compartment>() + .iter() + .map(|(entity, &compartment)| (entity, compartment)) + .collect(); + + for (_, (connection, current)) in world.query::<(&Connection, &CompartmentCurrent)>().iter() { + if let Ok(_compartment_to) = world.get::<&Compartment>(connection.to) { + if recently_fired.contains(&connection.from) { + let new_compartment_to = new_compartments + .get_mut(&connection.to) + .expect("Could not get new compartment"); + new_compartment_to.voltage = FHN_FIRE_VOLTAGE * FHN_SCALE + FHN_OFFSET; + } else if let Ok(compartment_from) = world.get::<&Compartment>(connection.from) { + let voltage_diff = compartment_from.voltage - _compartment_to.voltage; + let delta_voltage = voltage_diff / current.capacitance; + let new_compartment_to = new_compartments + .get_mut(&connection.to) + .expect("Could not get new compartment"); + new_compartment_to.voltage += delta_voltage * cdt; + let new_compartment_from = new_compartments + .get_mut(&connection.from) + .expect("Could not get new compartment"); + new_compartment_from.voltage -= delta_voltage * cdt; + } + } + } + + for (compartment_id, new_compartment) in new_compartments { + let mut old_compartment = world + .get::<&mut Compartment>(compartment_id) + .expect("Could not find compartment"); + *old_compartment = new_compartment; + } + + let compartment_to_neuron: Vec<(hecs::Entity, f64)> = world + .query::<&Connection>() + .with::<&CompartmentCurrent>() + .iter() + .filter_map(|(_, conn)| { + let compartment = world.get::<&Compartment>(conn.from).ok()?; + let excess = + (compartment.voltage - BRIDGE_VOLTAGE_THRESHOLD).clamp(0.0, BRIDGE_VOLTAGE_CLAMP); + if excess == 0.0 { + return None; + } + let current = excess / BRIDGE_VOLTAGE_CLAMP * BRIDGE_CURRENT_SCALE; + world.get::<&LeakyDynamics>(conn.to).ok()?; + let sign = match world.get::<&NeuronType>(conn.from) { + Ok(nt) => match *nt { + NeuronType::Excitatory => 1.0, + NeuronType::Inhibitory => -1.0, + }, + Err(_) => 1.0, + }; + Some((conn.to, sign * current)) + }) + .collect(); + + for (target, current) in compartment_to_neuron { + if let Ok(mut dynamics) = world.get::<&mut LeakyDynamics>(target) { + dynamics.received_currents += current; + } + } +} + +#[cfg(test)] +mod axon_tests { + use super::*; + use crate::simulation::lif::lif_step; + use glam::Vec3; + + #[test] + fn test_axon_action_potential_triggers_target_neuron() { + let mut world = hecs::World::new(); + + let neuron_a = world.spawn(( + LeakyNeuron::default(), + LeakyDynamics::default(), + LeakCurrent::default(), + Position { + position: Vec3::new(0.0, 0.0, 0.0), + }, + NeuronType::Excitatory, + )); + + let clamp = world.spawn(( + CurrentClamp { + current_output: 5e-9, + }, + Position { + position: Vec3::new(0.0, 0.0, -1.0), + }, + )); + world.spawn(( + Connection { + from: clamp, + to: neuron_a, + strength: 1.0, + directional: true, + }, + ImmediateFireSynapse::default(), + )); + + let neuron_b = world.spawn(( + LeakyNeuron::default(), + LeakyDynamics::default(), + LeakCurrent::default(), + Position { + position: Vec3::new(0.0, 0.0, 10.0), + }, + NeuronType::Excitatory, + )); + + let num_compartments = 5; + let mut compartment_entities = Vec::new(); + for i in 0..num_compartments { + let comp = world.spawn(( + Compartment { + voltage: -10.0, + m: -0.625, + h: 0.0, + n: 0.0, + influence: 0.0, + capacitance: 1.0, + injected_current: 0.0, + fire_impulse: 0.0, + }, + Position { + position: Vec3::new(0.0, 0.0, 2.0 * (i + 1) as f32), + }, + NeuronType::Excitatory, + )); + compartment_entities.push(comp); + } + + world.spawn(( + Connection { + from: neuron_a, + to: compartment_entities[0], + strength: 1.0, + directional: false, + }, + CompartmentCurrent { + capacitance: COUPLING_CAPACITANCE, + }, + )); + + for i in 0..num_compartments - 1 { + world.spawn(( + Connection { + from: compartment_entities[i], + to: compartment_entities[i + 1], + strength: 1.0, + directional: false, + }, + CompartmentCurrent { + capacitance: COUPLING_CAPACITANCE, + }, + )); + } + + world.spawn(( + Connection { + from: *compartment_entities.last().unwrap(), + to: neuron_b, + strength: 1.0, + directional: false, + }, + CompartmentCurrent { + capacitance: COUPLING_CAPACITANCE, + }, + )); + + let lif_dt = LIF_DT; + let cdt = FHN_CDT; + let lif_steps_per_frame = 10; + let total_fhn_steps = 2000; + + let mut time = 0.0; + let mut neuron_b_fired = false; + + for _ in 0..total_fhn_steps { + for _ in 0..lif_steps_per_frame { + lif_step(&mut world, lif_dt, time); + time += lif_dt; + } + + let recently_fired: std::collections::HashSet = world + .query::<&LeakyDynamics>() + .iter() + .filter(|(_, d)| d.time_since_fire < lif_steps_per_frame as f64 * lif_dt) + .map(|(e, _)| e) + .collect(); + + fhn_step(&mut world, cdt, &recently_fired); + + if let Ok(dynamics) = world.get::<&LeakyDynamics>(neuron_b) { + if dynamics.time_since_fire < lif_steps_per_frame as f64 * lif_dt { + neuron_b_fired = true; + } + } + } + + assert!( + neuron_b_fired, + "Target neuron should fire after action potential propagates through axon compartment chain" + ); + } +} diff --git a/neuronify-core/src/simulation/lif.rs b/neuronify-core/src/simulation/lif.rs new file mode 100644 index 00000000..c1e87eee --- /dev/null +++ b/neuronify-core/src/simulation/lif.rs @@ -0,0 +1,321 @@ +use hecs::World; +use rand::Rng; + +use crate::components::*; +use crate::measurement::voltmeter::{VoltageMeasurement, VoltageSeries, Voltmeter}; + +#[derive(Clone, Debug)] +pub struct SpikeRecord { + pub entity_index: usize, + pub time: f64, +} + +pub fn lif_step(world: &mut World, dt: f64, time: f64) { + for (_, (generator, dynamics)) in + world.query_mut::<(&RegularSpikeGenerator, &mut GeneratorDynamics)>() + { + dynamics.time_since_fire += dt; + if generator.frequency > 0.0 && dynamics.time_since_fire >= 1.0 / generator.frequency { + dynamics.fired = true; + dynamics.time_since_fire = 0.0; + } + } + + { + let mut rng = rand::thread_rng(); + let poisson_entities: Vec = world + .query::<&PoissonGenerator>() + .iter() + .map(|(e, _)| e) + .collect(); + for entity in poisson_entities { + let mut query = world + .query_one::<(&PoissonGenerator, &mut GeneratorDynamics)>(entity) + .unwrap(); + let (gen, dynamics) = query.get().unwrap(); + dynamics.time_since_fire += dt; + if gen.rate > 0.0 && rng.gen::() < gen.rate * dt { + dynamics.fired = true; + dynamics.time_since_fire = 0.0; + } + drop(query); + } + } + + let neuron_entities: Vec = world + .query::<&LeakyNeuron>() + .iter() + .map(|(e, _)| e) + .collect(); + + for entity in &neuron_entities { + let mut query = world + .query_one::<(&LeakyNeuron, &mut LeakyDynamics)>(*entity) + .unwrap(); + let (neuron, dynamics) = query.get().unwrap(); + + dynamics.time_since_fire += dt; + dynamics.enabled = dynamics.time_since_fire >= dynamics.refractory_period; + + if dynamics.enabled && dynamics.voltage > neuron.threshold { + dynamics.fired = true; + dynamics.voltage = neuron.initial_potential; + dynamics.time_since_fire = 0.0; + dynamics.enabled = false; + } + drop(query); + } + + for (_, (leak, neuron, dynamics)) in + world.query_mut::<(&mut LeakCurrent, &LeakyNeuron, &LeakyDynamics)>() + { + if !dynamics.enabled { + leak.current = 0.0; + continue; + } + let v = dynamics.voltage; + let em = neuron.resting_potential; + leak.current = -(v - em) / leak.resistance; + } + + for (_, (adapt, neuron, dynamics)) in + world.query_mut::<(&mut AdaptationCurrent, &LeakyNeuron, &LeakyDynamics)>() + { + if !dynamics.enabled { + adapt.current = 0.0; + continue; + } + adapt.conductance -= adapt.conductance / adapt.time_constant * dt; + if dynamics.fired { + adapt.conductance += adapt.adaptation; + } + let v = dynamics.voltage; + let em = neuron.resting_potential; + adapt.current = -adapt.conductance * (v - em); + } + + { + let leak_currents: Vec<(hecs::Entity, f64)> = world + .query::<(&LeakCurrent, &LeakyDynamics)>() + .iter() + .map(|(e, (leak, _))| (e, leak.current)) + .collect(); + + for (entity, current) in leak_currents { + if let Ok(mut dynamics) = world.get::<&mut LeakyDynamics>(entity) { + dynamics.received_currents += current; + } + } + } + + { + let adapt_currents: Vec<(hecs::Entity, f64)> = world + .query::<(&AdaptationCurrent, &LeakyDynamics)>() + .iter() + .map(|(e, (adapt, _))| (e, adapt.current)) + .collect(); + + for (entity, current) in adapt_currents { + if let Ok(mut dynamics) = world.get::<&mut LeakyDynamics>(entity) { + dynamics.received_currents += current; + } + } + } + + for (_, (neuron, dynamics)) in world.query_mut::<(&LeakyNeuron, &mut LeakyDynamics)>() { + if !dynamics.enabled { + dynamics.received_currents = 0.0; + continue; + } + + let total_current = dynamics.received_currents; + let dv = total_current / neuron.capacitance * dt; + dynamics.voltage += dv; + + if neuron.voltage_clamped { + dynamics.voltage = dynamics + .voltage + .clamp(neuron.minimum_voltage, neuron.maximum_voltage); + } + + dynamics.received_currents = 0.0; + } + + for (_, synapse) in world.query_mut::<&mut CurrentSynapse>() { + if synapse.alpha_function { + synapse.current_output = synapse.maximum_current * synapse.linear * synapse.exponential; + } else { + synapse.current_output = synapse.maximum_current * synapse.exponential; + } + + synapse.exponential -= synapse.exponential * dt / synapse.tau; + + if synapse.alpha_function { + synapse.linear += dt / synapse.tau; + } + + while !synapse.triggers.is_empty() && synapse.triggers[0] <= synapse.time { + synapse.triggers.remove(0); + if synapse.alpha_function { + synapse.linear = 0.0; + synapse.exponential = std::f64::consts::E; + } else { + synapse.exponential = 1.0; + } + } + + synapse.time += dt; + } + + for (_, synapse) in world.query_mut::<&mut ImmediateFireSynapse>() { + synapse.current_output = 0.0; + } + + let edges_with_fire: Vec<(hecs::Entity, hecs::Entity, hecs::Entity, bool)> = world + .query::<&Connection>() + .iter() + .map(|(edge_entity, conn)| { + let source_fired = world + .get::<&LeakyDynamics>(conn.from) + .map(|d| d.fired) + .unwrap_or(false) + || world + .get::<&GeneratorDynamics>(conn.from) + .map(|d| d.fired) + .unwrap_or(false); + (edge_entity, conn.from, conn.to, source_fired) + }) + .collect(); + + for (edge_entity, _source, _target, source_fired) in &edges_with_fire { + if !source_fired { + continue; + } + + if let Ok(mut synapse) = world.get::<&mut CurrentSynapse>(*edge_entity) { + if synapse.delay > 0.0 { + let trigger_time = synapse.time + synapse.delay; + synapse.triggers.push(trigger_time); + } else if synapse.alpha_function { + synapse.linear = 0.0; + synapse.exponential = std::f64::consts::E; + } else { + synapse.exponential = 1.0; + } + } + + if let Ok(mut synapse) = world.get::<&mut ImmediateFireSynapse>(*edge_entity) { + synapse.current_output = 1e6; + } + } + + let current_deliveries: Vec<(hecs::Entity, f64)> = edges_with_fire + .iter() + .filter_map(|(edge_entity, source, _target, _)| { + let sign = if world.get::<&Inhibitory>(*source).is_ok() { + -1.0 + } else { + 1.0 + }; + + let mut total = 0.0; + + if let Ok(synapse) = world.get::<&CurrentSynapse>(*edge_entity) { + if synapse.current_output != 0.0 { + total += sign * synapse.current_output; + } + } + + if let Ok(synapse) = world.get::<&ImmediateFireSynapse>(*edge_entity) { + if synapse.current_output != 0.0 { + total += sign * synapse.current_output; + } + } + + if let Ok(clamp) = world.get::<&CurrentClamp>(*source) { + if clamp.current_output != 0.0 { + total += sign * clamp.current_output; + } + } + + if total != 0.0 { + Some((*_target, total)) + } else { + None + } + }) + .collect(); + + for (target, current) in current_deliveries { + if let Ok(mut dynamics) = world.get::<&mut LeakyDynamics>(target) { + dynamics.received_currents += current; + } + } + + let voltmeter_updates: Vec<(hecs::Entity, f64, bool)> = world + .query::<(&Voltmeter, &Connection)>() + .iter() + .filter_map(|(entity, (_, conn))| { + if let Ok(dynamics) = world.get::<&LeakyDynamics>(conn.from) { + return Some(( + entity, + dynamics.voltage * 1000.0, + dynamics.time_since_fire == 0.0, + )); + } + if let Ok(compartment) = world.get::<&Compartment>(conn.from) { + return Some((entity, compartment.voltage, false)); + } + None + }) + .collect(); + + for (entity, voltage, fired) in voltmeter_updates { + if let Ok(mut series) = world.get::<&mut VoltageSeries>(entity) { + series + .measurements + .push(VoltageMeasurement { voltage, time }); + if fired { + series.spike_times.push(time); + } + } + } + + for (_, dynamics) in world.query_mut::<&mut LeakyDynamics>() { + dynamics.fired = false; + } + + for (_, dynamics) in world.query_mut::<&mut GeneratorDynamics>() { + dynamics.fired = false; + } +} + +pub fn run_headless(world: &mut World, steps: usize, dt: f64) -> Vec { + let mut spike_records = Vec::new(); + let mut time = 0.0; + + let neuron_entities: Vec = world + .query::<&LeakyNeuron>() + .iter() + .map(|(e, _)| e) + .collect(); + + for _step in 0..steps { + lif_step(world, dt, time); + + for (idx, entity) in neuron_entities.iter().enumerate() { + if let Ok(dynamics) = world.get::<&LeakyDynamics>(*entity) { + if dynamics.time_since_fire == 0.0 { + spike_records.push(SpikeRecord { + entity_index: idx, + time, + }); + } + } + } + + time += dt; + } + + spike_records +} diff --git a/neuronify-core/src/simulation/mod.rs b/neuronify-core/src/simulation/mod.rs new file mode 100644 index 00000000..1dbb86fb --- /dev/null +++ b/neuronify-core/src/simulation/mod.rs @@ -0,0 +1,9 @@ +pub mod fhn; +pub mod lif; +pub mod spatial; +pub mod stimulation; + +pub use fhn::fhn_step; +pub use lif::{lif_step, run_headless, SpikeRecord}; +pub use spatial::{apply_spatial_forces, integrate_motion}; +pub use stimulation::stimulate_nearby; diff --git a/neuronify-core/src/simulation/spatial.rs b/neuronify-core/src/simulation/spatial.rs new file mode 100644 index 00000000..03456bba --- /dev/null +++ b/neuronify-core/src/simulation/spatial.rs @@ -0,0 +1,104 @@ +use glam::Vec3; +use hecs::Entity; + +use crate::components::*; +use crate::constants::*; + +pub fn apply_spatial_forces(world: &mut hecs::World) { + let positions: Vec<(Entity, Position)> = world + .query::<&Position>() + .iter() + .map(|(e, p)| (e.to_owned(), p.to_owned())) + .collect(); + for (id, (position, dynamics)) in world.query_mut::<(&Position, &mut SpatialDynamics)>() { + for (other_id, other_position) in &positions { + if id == *other_id { + continue; + } + let from = position.position; + let to = other_position.position; + let r2 = from.distance_squared(to); + let target2 = (2.0 * NODE_RADIUS).powi(2); + let d = (to - from).normalize(); + let force = REPULSION_STRENGTH * (r2 - target2).min(0.0) * d; + dynamics.acceleration += force; + } + } + + let connections: Vec<(Entity, Connection)> = world + .query::<&Connection>() + .iter() + .map(|(e, c)| (e.to_owned(), c.to_owned())) + .collect(); + + for (connection_id_1, connection_1) in &connections { + for (connection_id_2, connection_2) in &connections { + if connection_id_1 == connection_id_2 { + continue; + } + if connection_1.to != connection_2.from { + continue; + } + let to_1 = world.get::<&Position>(connection_1.to).unwrap().position; + let from_1 = world.get::<&Position>(connection_1.from).unwrap().position; + let to_2 = world.get::<&Position>(connection_2.to).unwrap().position; + let from_2 = world.get::<&Position>(connection_2.from).unwrap().position; + let target = 1.0; + let dir_ab = (to_1 - from_1).normalize(); + let dir_bc = (to_2 - from_2).normalize(); + let dot = dir_ab.dot(dir_bc); + let diff = target - dot; + let p_a = (dir_ab.cross((dir_ab).cross(dir_bc))).normalize(); + let p_c = (dir_bc.cross((dir_ab).cross(dir_bc))).normalize(); + let k = ANGLE_ALIGNMENT_STRENGTH; + let f_a = k * diff / dir_ab.length() * p_a; + let f_c = k * diff / dir_bc.length() * p_c; + let f_b = -f_a - f_c; + if f_a.is_nan() || f_b.is_nan() || f_c.is_nan() { + continue; + } + if let Ok(mut dynamics_a) = world.get::<&mut SpatialDynamics>(connection_1.from) { + dynamics_a.acceleration += f_a; + } + if let Ok(mut dynamics_b) = world.get::<&mut SpatialDynamics>(connection_1.to) { + dynamics_b.acceleration += f_b; + } + if let Ok(mut dynamics_c) = world.get::<&mut SpatialDynamics>(connection_2.to) { + dynamics_c.acceleration += f_c; + } + } + } + + for (_, connection) in world + .query::<&Connection>() + .with::<&CompartmentCurrent>() + .iter() + { + if let (Ok(from), Ok(to)) = ( + world.get::<&Position>(connection.from), + world.get::<&Position>(connection.to), + ) { + let r2 = from.position.distance_squared(to.position); + let d = to.position - from.position; + let target_length = 2.0 * NODE_RADIUS; + let force = SPRING_STRENGTH * (r2 - target_length.powi(2)) * d.normalize(); + if let Ok(mut dynamics_from) = world.get::<&mut SpatialDynamics>(connection.from) { + dynamics_from.acceleration += force; + } + if let Ok(mut dynamics_to) = world.get::<&mut SpatialDynamics>(connection.to) { + dynamics_to.acceleration -= force; + } + } + } +} + +pub fn integrate_motion(world: &mut hecs::World, dt: f64) { + for (_, (position, dynamics)) in world.query_mut::<(&mut Position, &mut SpatialDynamics)>() { + let gravity = -position.position.y; + dynamics.acceleration += Vec3::new(0.0, gravity, 0.0); + dynamics.velocity += dynamics.acceleration * dt as f32; + position.position += dynamics.velocity * dt as f32; + dynamics.acceleration = Vec3::new(0.0, 0.0, 0.0); + dynamics.velocity -= dynamics.velocity * dt as f32; + } +} diff --git a/neuronify-core/src/simulation/stimulation.rs b/neuronify-core/src/simulation/stimulation.rs new file mode 100644 index 00000000..108929d5 --- /dev/null +++ b/neuronify-core/src/simulation/stimulation.rs @@ -0,0 +1,35 @@ +use crate::components::*; +use crate::constants::*; +use crate::StimulationTool; + +pub fn stimulate_nearby(world: &mut hecs::World, stimulation_tool: &Option) { + let Some(stim) = stimulation_tool else { + return; + }; + + let touch_entities: Vec = world + .query::<(&Position, &TouchSensor)>() + .iter() + .filter(|(_, (pos, _))| pos.position.distance(stim.position) < ERASE_RADIUS) + .map(|(e, _)| e) + .collect(); + for entity in touch_entities { + if let Ok(mut dynamics) = world.get::<&mut GeneratorDynamics>(entity) { + dynamics.fired = true; + dynamics.time_since_fire = 0.0; + } + } + + let neuron_entities: Vec = world + .query::<(&Position, &LeakyNeuron)>() + .iter() + .filter(|(_, (pos, _))| pos.position.distance(stim.position) < ERASE_RADIUS) + .map(|(e, _)| e) + .collect(); + for entity in neuron_entities { + if let Ok(mut dynamics) = world.get::<&mut LeakyDynamics>(entity) { + let neuron = world.get::<&LeakyNeuron>(entity).unwrap(); + dynamics.voltage = neuron.threshold + 0.01; + } + } +} diff --git a/neuronify-core/src/tools.rs b/neuronify-core/src/tools.rs new file mode 100644 index 00000000..36bfc2c3 --- /dev/null +++ b/neuronify-core/src/tools.rs @@ -0,0 +1,89 @@ +use glam::Vec3; +use hecs::Entity; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq)] +pub enum Tool { + Select, + ExcitatoryNeuron, + InhibitoryNeuron, + CurrentSource, + TouchSensor, + RegularSpikeGenerator, + PoissonGenerator, + Voltmeter, + StaticConnection, + Axon, + Erase, + Stimulate, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ToolCategory { + Interaction, + Neurons, + Connections, +} + +impl ToolCategory { + pub fn label(&self) -> &str { + match self { + ToolCategory::Interaction => "Interaction", + ToolCategory::Neurons => "Neurons", + ToolCategory::Connections => "Connections", + } + } + pub fn tools(&self) -> Vec<(Tool, &str)> { + match self { + ToolCategory::Interaction => vec![ + (Tool::Select, "Select"), + (Tool::Stimulate, "Stimulate"), + (Tool::Erase, "Erase"), + ], + ToolCategory::Neurons => vec![ + (Tool::ExcitatoryNeuron, "Excitatory Neuron"), + (Tool::InhibitoryNeuron, "Inhibitory Neuron"), + (Tool::CurrentSource, "Current Source"), + (Tool::TouchSensor, "Touch Sensor"), + (Tool::RegularSpikeGenerator, "Spike Generator"), + (Tool::PoissonGenerator, "Poisson Generator"), + (Tool::Voltmeter, "Voltmeter"), + ], + ToolCategory::Connections => vec![ + (Tool::StaticConnection, "Static Connection"), + (Tool::Axon, "Axon"), + ], + } + } +} + +pub const TOOL_CATEGORIES: [ToolCategory; 3] = [ + ToolCategory::Interaction, + ToolCategory::Neurons, + ToolCategory::Connections, +]; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PreviousCreation { + pub entity: Entity, +} + +#[derive(Clone, Debug)] +pub struct ConnectionTool { + pub start: Vec3, + pub end: Vec3, + pub from: Entity, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct StimulationTool { + pub position: Vec3, +} + +#[derive(Clone, Copy, Debug)] +pub enum ResizeCorner { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +}