diff --git a/Cargo.lock b/Cargo.lock index 973616c..83ce44d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[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", @@ -17,13 +17,22 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "anes" version = "0.1.6" @@ -32,21 +41,21 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cast" @@ -54,11 +63,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "ciborium" @@ -89,18 +108,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -108,31 +127,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "criterion" -version = "0.5.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", - "itertools 0.10.5", + "itertools 0.13.0", "num-traits", - "once_cell", "oorandom", + "page_size", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -140,19 +158,19 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -169,60 +187,86 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasip2", ] [[package]] name = "half" -version = "2.4.1" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "hypergraph" @@ -231,83 +275,73 @@ dependencies = [ "ahash", "criterion", "indexmap", - "itertools 0.13.0", + "itertools 0.14.0", "rayon", + "serde", + "serde_json", "thiserror", ] [[package]] name = "indexmap" -version = "2.6.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", "rayon", -] - -[[package]] -name = "is-terminal" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.52.0", + "serde", + "serde_core", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", + "once_cell", "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.164" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "num-traits" @@ -320,15 +354,31 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" -version = "11.1.4" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plotters" @@ -360,27 +410,33 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[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 = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -388,9 +444,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -398,9 +454,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -410,9 +466,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -421,15 +477,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "ryu" -version = "1.0.18" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -442,18 +498,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.215" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +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 = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -462,21 +528,34 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "syn" -version = "2.0.87" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -485,18 +564,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -515,9 +594,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "version_check" @@ -536,42 +615,32 @@ dependencies = [ ] [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -579,140 +648,110 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "windows-targets", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "windows-sys" -version = "0.59.0" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "windows-targets" -version = "0.52.6" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-sys", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 711f959..ee3faf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,25 +2,30 @@ authors = ["Davy Duperron "] categories = ["data-structures", "mathematics", "science"] description = "Hypergraph is data structure library to create a directed hypergraph in which an hyperedge can join any number of vertices." -edition = "2021" +edition = "2024" exclude = [".github", "hypergraph.svg", "rustfmt.toml"] keywords = ["data-structure", "graph", "hypergraph"] license = "MIT" name = "hypergraph" readme = "README.md" repository = "https://github.com/yamafaktory/hypergraph" -rust-version = "1.56" +rust-version = "1.95" version = "2.2.0" +[features] +serde = ["dep:serde", "indexmap/serde"] + [dependencies] -ahash = "0.8.11" -indexmap = { version = "2.6.0", features = ["rayon"] } -itertools = "0.13.0" -rayon = "1.7.0" -thiserror = "2.0.3" +ahash = "0.8.12" +indexmap = { version = "2.14.0", features = ["rayon"] } +itertools = "0.14.0" +rayon = "1.12.0" +serde = { version = "1.0.228", features = ["derive"], optional = true } +thiserror = "2.0.18" [dev-dependencies] -criterion = "0.5.1" +criterion = "0.8.2" +serde_json = "1.0.149" [[bench]] name = "performance" @@ -30,9 +35,10 @@ harness = false missing_debug_implementations = "warn" missing_docs = "warn" nonstandard_style = { level = "deny", priority= -1 } -rust_2021_compatibility = { level = "forbid", priority= -1 } +rust_2024_compatibility = { level = "forbid", priority= -1 } unreachable_pub = "warn" unsafe_code = "deny" [lints.clippy] all = "deny" +pedantic = "deny" diff --git a/README.md b/README.md index f7c1390..9ac6a68 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,24 @@ One of the main advantages of using a hypergraph model over a graph one is to pr This library enables you to represent: -- **non-simple** hypergraphs with two or more hyperedges - with different weights - containing the exact same set of vertices -- **self-loops** - i.e., hyperedges containing vertices directed to themselves one or more times -- **unaries** - i.e., hyperedges containing a unique vertex +- **non-simple** hypergraphs with two or more hyperedges containing the exact same set of vertices +- **self-loops** — i.e., hyperedges containing vertices directed to themselves one or more times +- **unaries** — i.e., hyperedges containing a unique vertex + +And to compute: + +- Graph traversal: **BFS**, **DFS**, reachability, topological sort +- Shortest paths: **Dijkstra** point-to-point and single-source +- Structural analysis: strongly connected components, weakly connected components, all simple paths, subgraph extraction, cycle detection +- Filtered views: `retain_vertices`, `retain_hyperedges` ## ⚗️ Implementation - 100% safe Rust - Proper error handling -- Stable indexes assigned for each hyperedge and each vertex +- Stable indexes for each hyperedge and each vertex — identity is the index, not the weight; duplicate weights are allowed on both sides - Parallelism (with Rayon) +- Optional `serde` support (`features = ["serde"]` in `Cargo.toml`) ## 🛠️ Installation diff --git a/benches/performance.rs b/benches/performance.rs index 3df63df..614c31a 100644 --- a/benches/performance.rs +++ b/benches/performance.rs @@ -1,4 +1,5 @@ #![deny(unsafe_code, nonstandard_style)] +#![allow(missing_docs)] use std::fmt::{ Display, @@ -7,6 +8,7 @@ use std::fmt::{ }; use criterion::{ + BatchSize, Criterion, criterion_group, criterion_main, @@ -18,36 +20,23 @@ use hypergraph::{ }; use itertools::Itertools; -static HYPEREDGES: usize = 10_000; -static VERTICES: usize = 10_000; +const VERTICES: usize = 1_000; #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] -pub struct Vertex(pub usize); - -impl Vertex { - pub fn new(rnd: usize) -> Self { - Vertex(rnd) - } -} +struct Vertex(usize); impl Display for Vertex { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "{}", self) + write!(f, "{}", self.0) } } #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] -pub struct Hyperedge(pub usize); - -impl Hyperedge { - pub fn new(rnd: usize) -> Self { - Hyperedge(rnd) - } -} +struct Hyperedge(usize); impl Display for Hyperedge { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "{}", self) + write!(f, "{}", self.0) } } @@ -57,52 +46,61 @@ impl From for usize { } } -fn criterion_benchmark(criterion: &mut Criterion) { +// Builds a linear-chain hypergraph: vertex 0 → 1 → 2 → … → VERTICES-1, +/// each consecutive pair joined by a hyperedge whose cost equals its index + 1. +fn build_graph() -> Hypergraph { let mut graph = Hypergraph::::new(); for i in 0..VERTICES { - graph.add_vertex(Vertex::new(i)).unwrap(); + graph.add_vertex(Vertex(i)).unwrap(); } - for i in 0..HYPEREDGES { - let vertices = (i..i + 1).map(VertexIndex).collect_vec(); - - graph.add_hyperedge(vertices, Hyperedge::new(i)).unwrap(); + for i in 0..VERTICES - 1 { + graph + .add_hyperedge(vec![VertexIndex(i), VertexIndex(i + 1)], Hyperedge(i + 1)) + .unwrap(); } + graph +} + +fn criterion_benchmark(criterion: &mut Criterion) { + let graph = build_graph(); + criterion.bench_function("get-hyperedge-vertices", |bencher| { - bencher.iter(|| graph.get_hyperedge_vertices(HyperedgeIndex((HYPEREDGES / 2) - 1))) + bencher.iter(|| graph.get_hyperedge_vertices(HyperedgeIndex(VERTICES / 2))) }); - criterion.bench_function("get-hyperedge-connecting", |bencher| { + criterion.bench_function("get-hyperedges-connecting", |bencher| { bencher.iter(|| { - graph.get_hyperedges_connecting( - VertexIndex((VERTICES / 2) - 1), - VertexIndex(VERTICES / 2), - ) + graph + .get_hyperedges_connecting(VertexIndex(VERTICES / 2), VertexIndex(VERTICES / 2 + 1)) }) }); - criterion.bench_function("get-all-hyperedges-intersections", |bencher| { - bencher.iter(|| { - graph.get_hyperedges_intersections((0..HYPEREDGES).map(HyperedgeIndex).collect_vec()) - }) + criterion.bench_function("get-hyperedges-intersections", |bencher| { + bencher + .iter(|| graph.get_hyperedges_intersections((0..10).map(HyperedgeIndex).collect_vec())) }); criterion.bench_function("dijkstra", |bencher| { - bencher.iter(|| graph.get_dijkstra_connections(VertexIndex(0), VertexIndex(VERTICES))) - }); - - criterion.bench_function("dijkstra-reversed", |bencher| { - bencher.iter(|| graph.get_dijkstra_connections(VertexIndex(VERTICES), VertexIndex(0))) + bencher.iter(|| graph.get_dijkstra_connections(VertexIndex(0), VertexIndex(VERTICES - 1))) }); criterion.bench_function("remove-vertex", |bencher| { - bencher.iter(|| graph.remove_vertex(VertexIndex(VERTICES))) + bencher.iter_batched( + build_graph, + |mut g| g.remove_vertex(VertexIndex(VERTICES - 1)), + BatchSize::LargeInput, + ) }); criterion.bench_function("remove-hyperedge", |bencher| { - bencher.iter(|| graph.remove_hyperedge(HyperedgeIndex(HYPEREDGES))) + bencher.iter_batched( + build_graph, + |mut g| g.remove_hyperedge(HyperedgeIndex(VERTICES - 2)), + BatchSize::LargeInput, + ) }); } diff --git a/rustfmt.toml b/rustfmt.toml index 0d8880c..676fe38 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ -edition = "2021" +edition = "2024" group_imports = "StdExternalCrate" imports_granularity = "Crate" imports_layout="Vertical" diff --git a/src/core/bi_hash_map.rs b/src/core/bi_hash_map.rs index e383b03..4588897 100644 --- a/src/core/bi_hash_map.rs +++ b/src/core/bi_hash_map.rs @@ -1,14 +1,17 @@ use std::{ collections::HashMap, fmt::Debug, + hash::Hash, }; /// Bi-directional hashmap used to store the mapping between the internal /// unstable indexes - generated by `IndexMap` and `IndexSet` - and the exposed /// stable indexes. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub(crate) struct BiHashMap where - Index: Copy + Debug + Eq, + Index: Copy + Debug + Eq + Hash, { pub(crate) left: HashMap, pub(crate) right: HashMap, @@ -16,7 +19,7 @@ where impl BiHashMap where - Index: Copy + Debug + Eq, + Index: Copy + Debug + Eq + Hash, { /// Creates a new `BiHashMap` with no allocation. pub(crate) fn new() -> BiHashMap { @@ -29,7 +32,7 @@ where impl Default for BiHashMap where - Index: Copy + Debug + Eq, + Index: Copy + Debug + Eq + Hash, { fn default() -> Self { BiHashMap::new() diff --git a/src/core/derive_trait.rs b/src/core/derive_trait.rs deleted file mode 100644 index af1c368..0000000 --- a/src/core/derive_trait.rs +++ /dev/null @@ -1,20 +0,0 @@ -extern crate proc_macro; -extern crate proc_macro2; -#[macro_use] -extern crate quote; -extern crate syn; - -use proc_macro2::TokenStream; - -#[proc_macro_attribute] -pub fn add_derive( - _metadata: proc_macro::TokenStream, - input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - let input: TokenStream = input.into(); - let output = quote! { - #[derive(Debug, Serialize, Deserialize, etc, ...)] - #input - }; - output.into() -} diff --git a/src/core/errors.rs b/src/core/errors.rs index f5b980e..2245af0 100644 --- a/src/core/errors.rs +++ b/src/core/errors.rs @@ -57,7 +57,12 @@ where vertices: Vec, }, - /// Error when a hyperedge is updated with the weight of another one. + /// Kept for API compatibility. + /// + /// Previously returned when a hyperedge was added or updated with a weight + /// already used by another hyperedge. The library no longer enforces + /// hyperedge-weight uniqueness, so this variant is never produced by any + /// public method. #[error("Hyperedge weight {0} was already assigned")] HyperedgeWeightAlreadyAssigned(HE), @@ -77,7 +82,10 @@ where #[error("Internal vertex index {0} was not found")] InternalVertexIndexNotFound(usize), - /// Error when a vertex weight was not found. + /// Kept for API compatibility. + /// + /// Previously used as an internal safe-check during vertex insertion. + /// No longer produced by any public method. #[error("Vertex weight {0} was not found")] VertexWeightNotFound(V), @@ -85,7 +93,16 @@ where #[error("VertexIndex {index:?} weight {weight:?} unchanged (no-op)")] VertexWeightUnchanged { index: VertexIndex, weight: V }, - /// Error when a vertex weight is updated with the weight of another one. + /// Kept for API compatibility. + /// + /// Previously returned when a vertex was added or updated with a weight + /// already used by another vertex. The library no longer enforces + /// vertex-weight uniqueness, so this variant is never produced by any + /// public method. #[error("Vertex weight {0} was already assigned")] VertexWeightAlreadyAssigned(V), + + /// Error when the hypergraph contains a cycle and a topological sort is requested. + #[error("Hypergraph contains a cycle and cannot be topologically sorted")] + HypergraphContainsCycle, } diff --git a/src/core/hyperedges/add_hyperedge.rs b/src/core/hyperedges/add_hyperedge.rs index ce63128..82c1771 100644 --- a/src/core/hyperedges/add_hyperedge.rs +++ b/src/core/hyperedges/add_hyperedge.rs @@ -13,8 +13,13 @@ where V: VertexTrait, HE: HyperedgeTrait, { - /// Adds a hyperedge as an array of vertices indexes and a custom weight in the hypergraph. - /// Returns the weighted index of the hyperedge. + /// Adds a hyperedge connecting `vertices` with the given `weight`. + /// Returns the stable index of the hyperedge. + /// + /// Duplicate weights are allowed — multiple hyperedges may carry the same + /// weight value. The unique key is the `(vertices, weight)` combination: if + /// an identical pair already exists the existing [`HyperedgeIndex`] is + /// returned without creating a duplicate entry. pub fn add_hyperedge( &mut self, vertices: Vec, @@ -27,19 +32,6 @@ where let internal_vertices = self.get_internal_vertices(vertices)?; - // Return an error if the weight is already assigned to another - // hyperedge. - // We can't use the contains method here since the key is a combination - // of the weight and the vertices. - if self.hyperedges.iter().any( - |HyperedgeKey { - weight: current_weight, - .. - }| { *current_weight == weight }, - ) { - return Err(HypergraphError::HyperedgeWeightAlreadyAssigned(weight)); - } - // We don't care about the second member of the tuple returned from // the insertion since this is an infallible operation. let (internal_index, _) = self @@ -50,7 +42,7 @@ where for vertex in internal_vertices { let (_, index_set) = self .vertices - .get_index_mut(vertex) + .get_mut(vertex) .ok_or(HypergraphError::InternalVertexIndexNotFound(vertex))?; index_set.insert(internal_index); diff --git a/src/core/hyperedges/contract_hyperedge_vertices.rs b/src/core/hyperedges/contract_hyperedge_vertices.rs index dc54214..3b31050 100644 --- a/src/core/hyperedges/contract_hyperedge_vertices.rs +++ b/src/core/hyperedges/contract_hyperedge_vertices.rs @@ -1,5 +1,4 @@ use itertools::Itertools; -use rayon::prelude::*; use crate::{ HyperedgeIndex, @@ -28,20 +27,13 @@ where // Get all the vertices of the hyperedge. let hyperedge_vertices = self.get_hyperedge_vertices(hyperedge_index)?; - // Get the deduped vertices. - // We use `par_sort_unstable` here which means that the order of equal - // elements is not preserved but this is fine since we dedupe them - // afterwards. let mut deduped_vertices = vertices; - deduped_vertices.par_sort_unstable(); + deduped_vertices.sort_unstable(); deduped_vertices.dedup(); // Check that the target is included in the deduped vertices. - if !deduped_vertices - .par_iter() - .any(|¤t_index| current_index == target) - { + if !deduped_vertices.contains(&target) { return Err(HypergraphError::HyperedgeInvalidContraction { index: hyperedge_index, target, @@ -51,18 +43,9 @@ where // Get the vertices not found in the hyperedge. let vertices_not_found = deduped_vertices - .par_iter() - .fold_with(vec![], |mut acc: Vec, &index| { - if !hyperedge_vertices - .par_iter() - .any(|¤t_index| current_index == index) - { - acc.push(index); - } - - acc - }) - .flatten() + .iter() + .filter(|index| !hyperedge_vertices.contains(index)) + .copied() .collect::>(); // Check that all the vertices - target included - are a subset of @@ -95,10 +78,7 @@ where .iter() // First remap each vertex to itself or to the target. .map(|vertex| { - if deduped_vertices - .par_iter() - .any(|¤t_index| current_index == *vertex) - { + if deduped_vertices.contains(vertex) { target } else { *vertex diff --git a/src/core/hyperedges/count_hyperedges.rs b/src/core/hyperedges/count_hyperedges.rs index 6472aef..1dd9932 100644 --- a/src/core/hyperedges/count_hyperedges.rs +++ b/src/core/hyperedges/count_hyperedges.rs @@ -10,6 +10,7 @@ where HE: HyperedgeTrait, { /// Returns the number of hyperedges in the hypergraph. + #[must_use] pub fn count_hyperedges(&self) -> usize { self.hyperedges.len() } diff --git a/src/core/hyperedges/find_hyperedges_by_weight.rs b/src/core/hyperedges/find_hyperedges_by_weight.rs new file mode 100644 index 0000000..09fb03a --- /dev/null +++ b/src/core/hyperedges/find_hyperedges_by_weight.rs @@ -0,0 +1,23 @@ +use crate::{ + HyperedgeIndex, + HyperedgeTrait, + Hypergraph, + VertexTrait, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the stable indexes of all hyperedges whose weight equals `weight`. + /// + /// Multiple hyperedges may share the same weight, so a `Vec` is returned. + /// Returns an empty `Vec` if no match is found. + #[must_use] + pub fn find_hyperedges_by_weight(&self, weight: HE) -> Vec { + self.hyperedges_iter() + .filter_map(|(idx, w)| (*w == weight).then_some(idx)) + .collect() + } +} diff --git a/src/core/hyperedges/join_hyperedges.rs b/src/core/hyperedges/join_hyperedges.rs index bbcdabd..8d8886d 100644 --- a/src/core/hyperedges/join_hyperedges.rs +++ b/src/core/hyperedges/join_hyperedges.rs @@ -1,5 +1,3 @@ -use rayon::prelude::*; - use crate::{ HyperedgeIndex, HyperedgeTrait, @@ -27,7 +25,7 @@ where // Try to collect all the vertices from the provided hyperedges. match hyperedges - .par_iter() + .iter() .map(|hyperedge_index| self.get_hyperedge_vertices(*hyperedge_index)) .collect::>, HypergraphError>>() { @@ -37,7 +35,7 @@ where // hyperedges to the first one. self.update_hyperedge_vertices( hyperedges[0], - joined_vertices.into_par_iter().flatten().collect(), + joined_vertices.into_iter().flatten().collect(), )?; // Get the tail. diff --git a/src/core/hyperedges/mod.rs b/src/core/hyperedges/mod.rs index 976964c..637be45 100644 --- a/src/core/hyperedges/mod.rs +++ b/src/core/hyperedges/mod.rs @@ -8,12 +8,14 @@ pub mod add_hyperedge; pub mod clear_hyperedges; pub mod contract_hyperedge_vertices; pub mod count_hyperedges; +pub mod find_hyperedges_by_weight; pub mod get_hyperedge_vertices; pub mod get_hyperedge_weight; pub mod get_hyperedges_connecting; pub mod get_hyperedges_intersections; pub mod join_hyperedges; pub mod remove_hyperedge; +pub mod retain_hyperedges; pub mod reverse_hyperedge; pub mod update_hyperedge_vertices; pub mod update_hyperedge_weight; diff --git a/src/core/hyperedges/remove_hyperedge.rs b/src/core/hyperedges/remove_hyperedge.rs index 86b7675..a8767db 100644 --- a/src/core/hyperedges/remove_hyperedge.rs +++ b/src/core/hyperedges/remove_hyperedge.rs @@ -36,7 +36,7 @@ where // Remove the hyperedge from the vertices. for vertex in vertices { - match self.vertices.get_index_mut(vertex) { + match self.vertices.get_mut(vertex) { Some((_, index_set)) => { index_set.swap_remove(&internal_index); } @@ -113,7 +113,7 @@ where // Update the impacted vertices accordingly. for vertex in swapped_vertices { - match self.vertices.get_index_mut(vertex) { + match self.vertices.get_mut(vertex) { Some((_, index_set)) => { // Perform an insertion of the current hyperedge and a // removal of the swapped one. diff --git a/src/core/hyperedges/retain_hyperedges.rs b/src/core/hyperedges/retain_hyperedges.rs new file mode 100644 index 0000000..9002a2a --- /dev/null +++ b/src/core/hyperedges/retain_hyperedges.rs @@ -0,0 +1,33 @@ +use crate::{ + HyperedgeIndex, + HyperedgeTrait, + Hypergraph, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Retains only the hyperedges for which `predicate` returns `true`. + /// + /// Vertices are unaffected. The predicate receives the stable index and a + /// reference to the weight of each hyperedge. + pub fn retain_hyperedges(&mut self, mut predicate: F) -> Result<(), HypergraphError> + where + F: FnMut(HyperedgeIndex, &HE) -> bool, + { + let to_remove: Vec = self + .hyperedges_iter() + .filter_map(|(idx, weight)| (!predicate(idx, weight)).then_some(idx)) + .collect(); + + for idx in to_remove { + self.remove_hyperedge(idx)?; + } + + Ok(()) + } +} diff --git a/src/core/hyperedges/reverse_hyperedge.rs b/src/core/hyperedges/reverse_hyperedge.rs index a6596c9..b5a30b1 100644 --- a/src/core/hyperedges/reverse_hyperedge.rs +++ b/src/core/hyperedges/reverse_hyperedge.rs @@ -13,15 +13,13 @@ where V: VertexTrait, HE: HyperedgeTrait, { - // Reverses a hyperedge. + /// Reverses the vertices of a hyperedge by index. pub fn reverse_hyperedge( &mut self, hyperedge_index: HyperedgeIndex, ) -> Result<(), HypergraphError> { - // Get the vertices of the hyperedge. let vertices = self.get_hyperedge_vertices(hyperedge_index)?; - // Update the hyperedge with the reversed vertices. self.update_hyperedge_vertices(hyperedge_index, vertices.into_par_iter().rev().collect()) } } diff --git a/src/core/hyperedges/update_hyperedge_vertices.rs b/src/core/hyperedges/update_hyperedge_vertices.rs index 959f5ea..67e1dbe 100644 --- a/src/core/hyperedges/update_hyperedge_vertices.rs +++ b/src/core/hyperedges/update_hyperedge_vertices.rs @@ -82,7 +82,7 @@ where // Update the added vertices. for index in added { - match self.vertices.get_index_mut(index) { + match self.vertices.get_mut(index) { Some((_, index_set)) => { index_set.insert(internal_index); } @@ -92,7 +92,7 @@ where // Update the removed vertices. for index in removed { - match self.vertices.get_index_mut(index) { + match self.vertices.get_mut(index) { Some((_, index_set)) => { // This has an impact on the internal indexing for the set. // However since this is not exposed to the user - i.e. no diff --git a/src/core/hyperedges/update_hyperedge_weight.rs b/src/core/hyperedges/update_hyperedge_weight.rs index a948087..82626ba 100644 --- a/src/core/hyperedges/update_hyperedge_weight.rs +++ b/src/core/hyperedges/update_hyperedge_weight.rs @@ -13,6 +13,10 @@ where HE: HyperedgeTrait, { /// Updates the weight of a hyperedge by index. + /// + /// The new weight need not be unique — multiple hyperedges may carry the + /// same weight value. Returns [`HypergraphError::HyperedgeWeightUnchanged`] + /// if `weight` equals the current weight (no-op guard). pub fn update_hyperedge_weight( &mut self, hyperedge_index: HyperedgeIndex, @@ -35,19 +39,6 @@ where }); } - // Return an error if the new weight is already assigned to another - // hyperedge. - // We can't use the contains method here since the key is a combination - // of the weight and the vertices. - if self.hyperedges.iter().any( - |HyperedgeKey { - weight: current_weight, - .. - }| { *current_weight == weight }, - ) { - return Err(HypergraphError::HyperedgeWeightAlreadyAssigned(weight)); - } - // IndexMap doesn't allow holes by design, see: // https://github.com/bluss/indexmap/issues/90#issuecomment-455381877 // @@ -86,9 +77,6 @@ where // +--+ // 2.Swap and remove // - // Insert the new entry. - // Since we have already checked that the new weight is not in the - // map, we can safely perform the operation without checking its output. self.hyperedges .insert(HyperedgeKey::new(vertices.clone(), weight)); diff --git a/src/core/indexes.rs b/src/core/indexes.rs index 0cfbf3c..dede338 100644 --- a/src/core/indexes.rs +++ b/src/core/indexes.rs @@ -8,6 +8,7 @@ use std::fmt::{ /// Uses the newtype index pattern. /// #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct VertexIndex(pub usize); impl Display for VertexIndex { @@ -26,6 +27,7 @@ impl From for VertexIndex { /// Uses the newtype index pattern. /// #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct HyperedgeIndex(pub usize); impl Display for HyperedgeIndex { diff --git a/src/core/iterator.rs b/src/core/iterator.rs index fa32570..c9c4e22 100644 --- a/src/core/iterator.rs +++ b/src/core/iterator.rs @@ -1,9 +1,9 @@ -use rayon::prelude::*; - use crate::{ + HyperedgeIndex, HyperedgeKey, HyperedgeTrait, Hypergraph, + VertexIndex, VertexTrait, errors::HypergraphError, }; @@ -24,8 +24,65 @@ where } } -/// Ideally we should be able to use GATs to expose `iter()`: -/// +impl<'a, V, HE> IntoIterator for &'a Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + type Item = (&'a HE, Vec<&'a V>); + type IntoIter = HypergraphBorrowingIterator<'a, V, HE>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns a borrowing iterator over the hyperedges as `(&HE, Vec<&V>)` tuples. + #[must_use] + pub fn iter(&self) -> HypergraphBorrowingIterator<'_, V, HE> { + HypergraphBorrowingIterator { + hypergraph: self, + index: 0, + } + } + + /// Returns an iterator over all vertices as `(VertexIndex, &V)` pairs, in insertion order. + #[must_use = "the iterator is lazy and must be consumed"] + pub fn vertices_iter(&self) -> impl Iterator + '_ { + self.vertices + .iter() + .enumerate() + .filter_map(|(internal, (weight, _))| { + self.vertices_mapping + .left + .get(&internal) + .copied() + .map(|stable| (stable, weight)) + }) + } + + /// Returns an iterator over all hyperedges as `(HyperedgeIndex, &HE)` pairs, in insertion order. + #[must_use = "the iterator is lazy and must be consumed"] + pub fn hyperedges_iter(&self) -> impl Iterator + '_ { + self.hyperedges + .iter() + .enumerate() + .filter_map(|(internal, key)| { + self.hyperedges_mapping + .left + .get(&internal) + .copied() + .map(|stable| (stable, &**key)) + }) + } +} + +/// A consuming iterator over a [`Hypergraph`]. #[derive(Debug)] pub struct HypergraphIterator where @@ -44,30 +101,64 @@ where type Item = (HE, Vec); fn next(&mut self) -> Option { - // Get the current hyperedge matching the index. - match self.hypergraph.hyperedges.iter().nth(self.index) { - // Extract the internal vertices and its weight. + match self.hypergraph.hyperedges.get_index(self.index) { Some(HyperedgeKey { vertices, weight }) => { - // Convert the internal vertices to a vector of VertexIndex. - // Since this is a fallible operation and we can't deal with a - // Result within this iterator, remap to None on error. if let Ok(indexes) = self.hypergraph.get_vertices(&vertices.clone()) { indexes - .par_iter() + .iter() .map(|index| self.hypergraph.get_vertex_weight(*index)) .collect::, HypergraphError>>() .ok() .map(|vertices_weights| { - // Now we can increment the inner index. self.index += 1; - - (*weight, vertices_weights.into_par_iter().cloned().collect()) + (*weight, vertices_weights.into_iter().copied().collect()) }) } else { None } } + None => None, + } + } +} + +/// A borrowing iterator over a [`Hypergraph`]. +#[derive(Debug)] +pub struct HypergraphBorrowingIterator<'s, V, HE> +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + hypergraph: &'s Hypergraph, + index: usize, +} + +impl<'s, V, HE> Iterator for HypergraphBorrowingIterator<'s, V, HE> +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + type Item = (&'s HE, Vec<&'s V>); + fn next(&mut self) -> Option { + match self.hypergraph.hyperedges.get_index(self.index) { + Some(HyperedgeKey { vertices, weight }) => { + let hypergraph = self.hypergraph; + + if let Ok(indexes) = hypergraph.get_vertices(vertices) { + indexes + .iter() + .map(|index| hypergraph.get_vertex_weight(*index)) + .collect::, HypergraphError>>() + .ok() + .map(|vertex_weights| { + self.index += 1; + (weight, vertex_weights) + }) + } else { + None + } + } None => None, } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 06e4781..3262bae 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -26,7 +26,6 @@ use std::{ use bi_hash_map::BiHashMap; use types::{ - AIndexMap, AIndexSet, ARandomState, }; @@ -36,6 +35,10 @@ pub use crate::core::indexes::{ HyperedgeIndex, VertexIndex, }; +pub use crate::core::iterator::{ + HypergraphBorrowingIterator, + HypergraphIterator, +}; /// Shared Trait for the vertices. /// Must be implemented to use the library. @@ -55,6 +58,7 @@ impl HyperedgeTrait for T where T: VertexTrait + Into {} /// different hyperedges, the weight is also included in the key to keep /// it unique. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub(crate) struct HyperedgeKey { vertices: Vec, weight: HE, @@ -76,11 +80,20 @@ impl Deref for HyperedgeKey { } /// A directed hypergraph composed of generic vertices and hyperedges. +#[derive(Clone)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = "V: serde::Serialize, HE: serde::Serialize", + deserialize = "V: serde::Deserialize<'de>, HE: serde::Deserialize<'de> + Eq + std::hash::Hash" + )) +)] pub struct Hypergraph { - /// Vertices are stored as a map whose unique keys are the weights - /// and the values are a set of the hyperedges indexes which include - /// the current vertex. - vertices: AIndexMap>, + /// Vertices are stored as a vec of `(weight, hyperedge-index-set)` pairs. + /// Position in the vec is the internal index. Weights are not required to + /// be unique — identity is the stable `VertexIndex`, not the weight. + vertices: Vec<(V, AIndexSet)>, /// Hyperedges are stored as a set whose unique keys are a combination of /// vertices indexes and a weight. Two or more hyperedges can contain @@ -102,7 +115,7 @@ pub struct Hypergraph { impl Debug for Hypergraph where - V: Eq + Hash + Debug, + V: Debug, HE: Debug, { fn fmt(&self, f: &mut Formatter<'_>) -> Result { @@ -113,6 +126,49 @@ where } } +impl Display for Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let mut vertices: Vec<(VertexIndex, &V)> = self.vertices_iter().collect(); + vertices.sort_by_key(|(idx, _)| *idx); + + write!(f, "Hypergraph {{ vertices: [")?; + for (i, (idx, weight)) in vertices.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", idx.0, weight)?; + } + + write!(f, "], hyperedges: [")?; + let mut hyperedges: Vec<(HyperedgeIndex, &HE)> = self.hyperedges_iter().collect(); + hyperedges.sort_by_key(|(idx, _)| *idx); + + for (i, (idx, weight)) in hyperedges.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {} [", idx.0, weight)?; + if let Ok(vertex_indexes) = self.get_hyperedge_vertices(*idx) { + for (j, v_idx) in vertex_indexes.iter().enumerate() { + if j > 0 { + write!(f, " → ")?; + } + if let Ok(v_weight) = self.get_vertex_weight(*v_idx) { + write!(f, "{v_weight}")?; + } + } + } + write!(f, "]")?; + } + + write!(f, "] }}") + } +} + impl Default for Hypergraph where V: VertexTrait, @@ -129,6 +185,15 @@ where V: VertexTrait, HE: HyperedgeTrait, { + /// Returns `true` if the hypergraph contains no vertices. + /// + /// Because hyperedges require at least one vertex to exist, an empty vertex + /// set implies an empty hyperedge set as well. + #[must_use] + pub fn is_empty(&self) -> bool { + self.vertices.is_empty() + } + /// Clears the hypergraph. pub fn clear(&mut self) { // Clear the hyperedges and vertices sets while keeping their capacities. @@ -145,11 +210,13 @@ where } /// Creates a new hypergraph with no allocation. + #[must_use] pub fn new() -> Self { Hypergraph::with_capacity(0, 0) } /// Creates a new hypergraph with the specified capacity. + #[must_use] pub fn with_capacity(vertices: usize, hyperedges: usize) -> Self { Hypergraph { hyperedges_count: 0, @@ -157,7 +224,7 @@ where hyperedges: AIndexSet::with_capacity_and_hasher(hyperedges, ARandomState::default()), vertices_count: 0, vertices_mapping: BiHashMap::default(), - vertices: AIndexMap::with_capacity_and_hasher(vertices, ARandomState::default()), + vertices: Vec::with_capacity(vertices), } } } diff --git a/src/core/shared.rs b/src/core/shared.rs index 06f55d8..c3be337 100644 --- a/src/core/shared.rs +++ b/src/core/shared.rs @@ -42,11 +42,11 @@ where let (_, hyperedges_index_set) = self .vertices - .get_index(internal_index) + .get(internal_index) .ok_or(HypergraphError::InternalVertexIndexNotFound(internal_index))?; let hyperedges = - self.get_hyperedges(&hyperedges_index_set.clone().into_iter().collect_vec())?; + self.get_hyperedges(&hyperedges_index_set.iter().copied().collect_vec())?; let hyperedges_with_vertices = hyperedges .into_par_iter() @@ -65,43 +65,25 @@ where |acc, (hyperedge_index, vertices)| { vertices.iter().tuple_windows::<(_, _)>().fold( acc, - |index_acc, (window_from, window_to)| { + |mut acc, (window_from, window_to)| { match connections { Connection::In(from) => { - // Inject the index of the hyperedge and the - // vertex index if the current window is a - // match. if *window_from == *from { - return index_acc - .into_iter() - .chain(vec![(hyperedge_index, Some(*window_to))]) - .collect_vec(); + acc.push((hyperedge_index, Some(*window_to))); } } Connection::Out(to) => { - // Inject the index of the hyperedge and the - // vertex index if the current window is a - // match. if *window_to == *to { - return index_acc - .into_iter() - .chain(vec![(hyperedge_index, Some(*window_from))]) - .collect_vec(); + acc.push((hyperedge_index, Some(*window_from))); } } Connection::InAndOut(from, to) => { - // Inject only the index of the hyperedge - // if the current window is a match. if *window_from == *from && *window_to == *to { - return index_acc - .into_iter() - .chain(vec![(hyperedge_index, None)]) - .collect_vec(); + acc.push((hyperedge_index, None)); } } } - - index_acc + acc }, ) }, diff --git a/src/core/types.rs b/src/core/types.rs index 783b726..b878335 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -1,11 +1,5 @@ use ahash::RandomState; -use indexmap::{ - IndexMap, - IndexSet, -}; - -/// Type alias to use `AHash` as a faster hasher for `IndexMap`. -pub(crate) type AIndexMap = IndexMap; +use indexmap::IndexSet; /// Type alias to use `AHash` as a faster hasher for `IndexSet`. pub(crate) type AIndexSet = IndexSet; diff --git a/src/core/utils.rs b/src/core/utils.rs index 2090b0e..1219212 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -1,12 +1,5 @@ -use itertools::Itertools; - pub(crate) fn are_slices_equal(a: &[usize], b: &[usize]) -> bool { - // Early guard if lengths are different. - if a.len() != b.len() { - return false; - } - - a.iter().zip_eq(b).fold(true, |acc, (a, b)| acc && a == b) + a == b } #[cfg(test)] diff --git a/src/core/vertices/add_vertex.rs b/src/core/vertices/add_vertex.rs index f7a9bf8..0cd0211 100644 --- a/src/core/vertices/add_vertex.rs +++ b/src/core/vertices/add_vertex.rs @@ -17,26 +17,15 @@ where { /// Adds a vertex with a custom weight to the hypergraph. /// Returns the index of the vertex. + /// + /// Duplicate weights are allowed — vertex identity is the returned + /// [`VertexIndex`], not the weight value. pub fn add_vertex(&mut self, weight: V) -> Result> { - // Return an error if the weight is already assigned to another vertex. - if self.vertices.contains_key(&weight) { - return Err(HypergraphError::VertexWeightAlreadyAssigned(weight)); - } - - self.vertices - .entry(weight) - .or_insert(AIndexSet::with_capacity_and_hasher( - 0, - ARandomState::default(), - )); - - let internal_index = self - .vertices - .get_index_of(&weight) - // This safe-check should always pass since the weight has been - // inserted upfront. - .ok_or(HypergraphError::VertexWeightNotFound(weight))?; - + let internal_index = self.vertices.len(); + self.vertices.push(( + weight, + AIndexSet::with_capacity_and_hasher(0, ARandomState::default()), + )); Ok(self.add_vertex_index(internal_index)) } } diff --git a/src/core/vertices/bfs.rs b/src/core/vertices/bfs.rs new file mode 100644 index 0000000..da70c20 --- /dev/null +++ b/src/core/vertices/bfs.rs @@ -0,0 +1,46 @@ +use std::collections::VecDeque; + +use ahash::AHashSet; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the vertices reachable from `from` in breadth-first order. + /// + /// The starting vertex is always the first element of the result. + /// Only vertices reachable via directed hyperedges are included. + /// + /// Returns [`HypergraphError::VertexIndexNotFound`] if `from` does not exist. + pub fn get_bfs(&self, from: VertexIndex) -> Result, HypergraphError> { + self.get_internal_vertex(from)?; + + let mut visited: AHashSet = AHashSet::new(); + let mut queue: VecDeque = VecDeque::new(); + let mut result: Vec = Vec::new(); + + visited.insert(from); + queue.push_back(from); + + while let Some(current) = queue.pop_front() { + result.push(current); + + for neighbor in self.get_adjacent_vertices_from(current)? { + if visited.insert(neighbor) { + queue.push_back(neighbor); + } + } + } + + Ok(result) + } +} diff --git a/src/core/vertices/connected_components.rs b/src/core/vertices/connected_components.rs new file mode 100644 index 0000000..6b9a4d9 --- /dev/null +++ b/src/core/vertices/connected_components.rs @@ -0,0 +1,67 @@ +use std::collections::VecDeque; + +use ahash::AHashSet; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the weakly connected components of the hypergraph. + /// + /// Each component is a sorted `Vec` of vertices that are + /// mutually reachable when edge direction is ignored. Isolated vertices + /// form their own single-element component. The outer `Vec` is sorted by + /// the smallest index in each component, giving a deterministic result. + /// + /// Returns an empty `Vec` for an empty hypergraph. + pub fn connected_components(&self) -> Result>, HypergraphError> { + let mut all_vertices: Vec = + self.vertices_mapping.left.values().copied().collect(); + all_vertices.sort(); + + let mut visited: AHashSet = AHashSet::new(); + let mut components: Vec> = Vec::new(); + + for start in all_vertices { + if visited.contains(&start) { + continue; + } + + let mut component: Vec = Vec::new(); + let mut queue: VecDeque = VecDeque::new(); + + visited.insert(start); + queue.push_back(start); + + while let Some(current) = queue.pop_front() { + component.push(current); + + // Treat edges as undirected: follow both outgoing and incoming. + for neighbor in self.get_adjacent_vertices_from(current)? { + if visited.insert(neighbor) { + queue.push_back(neighbor); + } + } + for neighbor in self.get_adjacent_vertices_to(current)? { + if visited.insert(neighbor) { + queue.push_back(neighbor); + } + } + } + + component.sort(); + components.push(component); + } + + Ok(components) + } +} diff --git a/src/core/vertices/contains_vertex.rs b/src/core/vertices/contains_vertex.rs new file mode 100644 index 0000000..9741f22 --- /dev/null +++ b/src/core/vertices/contains_vertex.rs @@ -0,0 +1,17 @@ +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexTrait, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns `true` if at least one vertex with the given weight exists. + #[must_use] + pub fn contains_vertex(&self, weight: V) -> bool { + self.vertices.iter().any(|(w, _)| *w == weight) + } +} diff --git a/src/core/vertices/count_vertices.rs b/src/core/vertices/count_vertices.rs index 4b713e9..18ce05e 100644 --- a/src/core/vertices/count_vertices.rs +++ b/src/core/vertices/count_vertices.rs @@ -10,6 +10,7 @@ where HE: HyperedgeTrait, { /// Returns the number of vertices in the hypergraph. + #[must_use] pub fn count_vertices(&self) -> usize { self.vertices.len() } diff --git a/src/core/vertices/dfs.rs b/src/core/vertices/dfs.rs new file mode 100644 index 0000000..eca2f21 --- /dev/null +++ b/src/core/vertices/dfs.rs @@ -0,0 +1,44 @@ +use ahash::AHashSet; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the vertices reachable from `from` in depth-first order. + /// + /// The starting vertex is always the first element of the result. + /// Only vertices reachable via directed hyperedges are included. + /// + /// Returns [`HypergraphError::VertexIndexNotFound`] if `from` does not exist. + pub fn get_dfs(&self, from: VertexIndex) -> Result, HypergraphError> { + self.get_internal_vertex(from)?; + + let mut visited: AHashSet = AHashSet::new(); + let mut stack: Vec = vec![from]; + let mut result: Vec = Vec::new(); + + while let Some(current) = stack.pop() { + if visited.insert(current) { + result.push(current); + + let neighbors = self.get_adjacent_vertices_from(current)?; + for neighbor in neighbors.into_iter().rev() { + if !visited.contains(&neighbor) { + stack.push(neighbor); + } + } + } + } + + Ok(result) + } +} diff --git a/src/core/vertices/get_all_paths.rs b/src/core/vertices/get_all_paths.rs new file mode 100644 index 0000000..479f6de --- /dev/null +++ b/src/core/vertices/get_all_paths.rs @@ -0,0 +1,85 @@ +use ahash::AHashSet; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns all simple paths (no repeated vertices) from `from` to `to`. + /// + /// Each path is a `Vec` that includes both endpoints. When + /// `from == to` the result is `vec![vec![from]]`. Paths are emitted in + /// DFS discovery order and are not sorted. + /// + /// **Warning**: the number of simple paths can grow exponentially with + /// graph size. Use on large or dense graphs with care. + /// + /// Returns [`HypergraphError::VertexIndexNotFound`] if either index does not + /// exist. + pub fn get_all_paths( + &self, + from: VertexIndex, + to: VertexIndex, + ) -> Result>, HypergraphError> { + self.get_internal_vertex(from)?; + self.get_internal_vertex(to)?; + + if from == to { + return Ok(vec![vec![from]]); + } + + let mut all_paths: Vec> = Vec::new(); + let mut current_path: Vec = vec![from]; + let mut visited: AHashSet = AHashSet::from([from]); + + // Each frame: (current vertex, its neighbors, next-neighbor index). + // Pushing a frame means we have already added `current` to `current_path` + // and `visited`. Popping a frame undoes both. + let mut stack: Vec<(VertexIndex, Vec, usize)> = + vec![(from, self.get_adjacent_vertices_from(from)?, 0)]; + + while let Some(frame) = stack.last_mut() { + let (current, neighbors, idx) = frame; + let current = *current; + + if *idx >= neighbors.len() { + // All neighbors of `current` explored — backtrack. + stack.pop(); + current_path.pop(); + visited.remove(¤t); + continue; + } + + let next = neighbors[*idx]; + *idx += 1; + + if visited.contains(&next) { + continue; + } + + if next == to { + let mut path = current_path.clone(); + path.push(to); + all_paths.push(path); + // Do not push `to` onto the stack: paths beyond the destination + // are not simple paths to `to`. + continue; + } + + visited.insert(next); + current_path.push(next); + let next_neighbors = self.get_adjacent_vertices_from(next)?; + stack.push((next, next_neighbors, 0)); + } + + Ok(all_paths) + } +} diff --git a/src/core/vertices/get_dijkstra_connections.rs b/src/core/vertices/get_dijkstra_connections.rs index bbae907..a6ba5e9 100644 --- a/src/core/vertices/get_dijkstra_connections.rs +++ b/src/core/vertices/get_dijkstra_connections.rs @@ -1,13 +1,10 @@ use std::{ cmp::Ordering, - collections::{ - BinaryHeap, - HashMap, - }, - fmt::Debug, + collections::BinaryHeap, + iter::successors, }; -use rayon::prelude::*; +use ahash::AHashMap; use crate::{ HyperedgeIndex, @@ -36,7 +33,7 @@ impl Ord for Visitor { other .distance .cmp(&self.distance) - .then_with(|| self.distance.cmp(&other.distance)) + .then_with(|| self.index.cmp(&other.index)) } } @@ -46,131 +43,116 @@ impl PartialOrd for Visitor { } } +type DijkstraResult = + Result<(usize, Vec<(VertexIndex, Option)>), HypergraphError>; + #[allow(clippy::type_complexity)] impl Hypergraph where V: VertexTrait, HE: HyperedgeTrait, { - /// Gets a list of the cheapest path of vertices between two vertices as a - /// vector of tuples of the form `(VertexIndex, Option)` - /// where the second member is the hyperedge that has been traversed to - /// reach the vertex. - /// Please note that the initial tuple holds `None` as hyperedge since none - /// has been traversed yet. - /// The implementation of the algorithm is partially based on: - /// - pub fn get_dijkstra_connections( - &self, - from: VertexIndex, - to: VertexIndex, - ) -> Result)>, HypergraphError> { - // Get the internal indexes of the vertices. + fn dijkstra_impl(&self, from: VertexIndex, to: VertexIndex) -> DijkstraResult { let internal_from = self.get_internal_vertex(from)?; let internal_to = self.get_internal_vertex(to)?; - // Keep track of the distances. - let mut distances = HashMap::new(); - - let mut maybe_traversed_hyperedge_by_vertex = HashMap::new(); - - // Create an empty binary heap. + let mut distances: AHashMap = AHashMap::new(); + // Maps each internal vertex index to its (predecessor, hyperedge used to arrive). + let mut predecessors: AHashMap)> = AHashMap::new(); let mut to_traverse = BinaryHeap::new(); - // Initialize the first vertex to zero. distances.insert(internal_from, 0); - - // Push the first cursor to the heap. to_traverse.push(Visitor::new(0, internal_from)); - // Keep track of the traversal path. - let mut path = Vec::::new(); - while let Some(Visitor { distance, index }) = to_traverse.pop() { - // End of the traversal. if index == internal_to { - // Inject the target vertex. - path.push(self.get_vertex(internal_to)?); - - return Ok(path - .into_par_iter() - .map(|vertex_index| { - ( - vertex_index, - maybe_traversed_hyperedge_by_vertex - .get(&vertex_index) - .and_then(|¤t| current), - ) - }) - .collect()); + // Walk the predecessor chain from destination back to source, + // then reverse to get source-to-destination order. + let path = successors(Some(internal_to), |¤t| { + (current != internal_from).then(|| predecessors[¤t].0) + }) + .collect::>() + .into_iter() + .rev() + .map(|internal| { + Ok(( + self.get_vertex(internal)?, + predecessors.get(&internal).and_then(|&(_, he)| he), + )) + }) + .collect::, HypergraphError>>()?; + + return Ok((distance, path)); } - // Skip if a better path has already been found. + // Skip stale heap entries. if distance > distances[&index] { continue; } - // Get the VertexIndex associated with the internal index. - // Proceed by finding all the adjacent vertices as a hashmap whose - // keys are VertexIndex and values are a vector of HyperedgeIndex. let mapped_index = self.get_vertex(index)?; - let indexes = self.get_full_adjacent_vertices_from(mapped_index)?; + let neighbors = self.get_full_adjacent_vertices_from(mapped_index)?; - // For every connected vertex, try to find the lowest distance. - for (vertex_index, hyperedge_indexes) in indexes { - let internal_vertex_index = self.get_internal_vertex(vertex_index)?; + for (vertex_index, hyperedge_indexes) in neighbors { + let internal_neighbor = self.get_internal_vertex(vertex_index)?; + // Find the minimum-cost hyperedge to this neighbor. let mut min_cost = usize::MAX; let mut best_hyperedge: Option = None; - // Get the lower cost out of all the hyperedges. for hyperedge_index in hyperedge_indexes { - let hyperedge_weight = self.get_hyperedge_weight(hyperedge_index)?; - - // Use the trait implementation to get the associated cost - // of the hyperedge. - let cost = hyperedge_weight.to_owned().into(); + let cost: usize = self + .get_hyperedge_weight(hyperedge_index)? + .to_owned() + .into(); if cost < min_cost { min_cost = cost; best_hyperedge = Some(hyperedge_index); - - break; } } - // Prepare the next visitor. - let next = Visitor::new(distance + min_cost, internal_vertex_index); - - // Check if this is the shorter distance. + let next_distance = distance + min_cost; let is_shorter = distances - .get(&next.index) - .map_or(true, |¤t| next.distance < current); + .get(&internal_neighbor) + .is_none_or(|¤t| next_distance < current); - // If so, add it to the frontier and continue. if is_shorter { - maybe_traversed_hyperedge_by_vertex.insert(vertex_index, best_hyperedge); - - // Update the path traversal accordingly. - // Keep vertex indexes unique. - if !path - .par_iter() - .any(|current_index| mapped_index == *current_index) - { - path.push(mapped_index); - } - - // Push it to the heap. - to_traverse.push(next); - - // Relaxation, we have now found a better way - distances.insert(internal_vertex_index, next.distance); + distances.insert(internal_neighbor, next_distance); + predecessors.insert(internal_neighbor, (index, best_hyperedge)); + to_traverse.push(Visitor::new(next_distance, internal_neighbor)); } } } - // If we reach this point, this means that there's no solution. - // Return an empty vector. - Ok(vec![]) + Ok((0, vec![])) + } + + /// Gets the cheapest path between two vertices as a vector of + /// `(VertexIndex, Option)` tuples. + /// + /// The first element always carries `None` as no hyperedge has been + /// traversed to reach the starting vertex. + /// The implementation is based on: + /// + pub fn get_dijkstra_connections( + &self, + from: VertexIndex, + to: VertexIndex, + ) -> Result)>, HypergraphError> { + self.dijkstra_impl(from, to).map(|(_, path)| path) + } + + /// Gets the cheapest path between two vertices together with the total cost. + /// + /// Returns `(total_cost, path)` where `path` is the same format as + /// [`get_dijkstra_connections`](Self::get_dijkstra_connections). + /// When no path exists, returns `(0, [])`. + pub fn get_dijkstra_connections_with_cost( + &self, + from: VertexIndex, + to: VertexIndex, + ) -> Result<(usize, Vec<(VertexIndex, Option)>), HypergraphError> { + self.dijkstra_impl(from, to) } } diff --git a/src/core/vertices/get_dijkstra_from.rs b/src/core/vertices/get_dijkstra_from.rs new file mode 100644 index 0000000..25d60f3 --- /dev/null +++ b/src/core/vertices/get_dijkstra_from.rs @@ -0,0 +1,106 @@ +use std::{ + cmp::Ordering, + collections::BinaryHeap, +}; + +use ahash::AHashMap; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Visitor { + distance: usize, + index: usize, +} + +impl Visitor { + fn new(distance: usize, index: usize) -> Self { + Self { distance, index } + } +} + +impl Ord for Visitor { + fn cmp(&self, other: &Visitor) -> Ordering { + other + .distance + .cmp(&self.distance) + .then_with(|| self.index.cmp(&other.index)) + } +} + +impl PartialOrd for Visitor { + fn partial_cmp(&self, other: &Visitor) -> Option { + Some(self.cmp(other)) + } +} + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the minimum cost to reach every vertex reachable from `from`. + /// + /// The result is a map of `VertexIndex → cost`. The source vertex itself + /// is always included with cost `0`. Vertices not reachable from `from` + /// are absent from the map. + /// + /// Returns [`HypergraphError::VertexIndexNotFound`] if `from` does not exist. + pub fn get_dijkstra_from( + &self, + from: VertexIndex, + ) -> Result, HypergraphError> { + let internal_from = self.get_internal_vertex(from)?; + + let mut distances: AHashMap = AHashMap::new(); + let mut to_traverse = BinaryHeap::new(); + + distances.insert(internal_from, 0); + to_traverse.push(Visitor::new(0, internal_from)); + + while let Some(Visitor { distance, index }) = to_traverse.pop() { + if distance > distances[&index] { + continue; + } + + let mapped_index = self.get_vertex(index)?; + let neighbors = self.get_full_adjacent_vertices_from(mapped_index)?; + + for (vertex_index, hyperedge_indexes) in neighbors { + let internal_neighbor = self.get_internal_vertex(vertex_index)?; + + let mut min_cost = usize::MAX; + for hyperedge_index in hyperedge_indexes { + let cost: usize = self + .get_hyperedge_weight(hyperedge_index)? + .to_owned() + .into(); + if cost < min_cost { + min_cost = cost; + } + } + + let next_distance = distance + min_cost; + let is_shorter = distances + .get(&internal_neighbor) + .is_none_or(|¤t| next_distance < current); + + if is_shorter { + distances.insert(internal_neighbor, next_distance); + to_traverse.push(Visitor::new(next_distance, internal_neighbor)); + } + } + } + + distances + .into_iter() + .map(|(internal, dist)| self.get_vertex(internal).map(|stable| (stable, dist))) + .collect() + } +} diff --git a/src/core/vertices/get_full_adjacent_vertices_from.rs b/src/core/vertices/get_full_adjacent_vertices_from.rs index 84dacce..2bd417e 100644 --- a/src/core/vertices/get_full_adjacent_vertices_from.rs +++ b/src/core/vertices/get_full_adjacent_vertices_from.rs @@ -21,7 +21,7 @@ where HE: HyperedgeTrait, { /// Gets the list of all vertices connected from a given vertex as tuples - /// of the form (`VertexIndex`, Vec). + /// of the form (`VertexIndex`, `Vec`). pub fn get_full_adjacent_vertices_from( &self, from: VertexIndex, diff --git a/src/core/vertices/get_full_adjacent_vertices_to.rs b/src/core/vertices/get_full_adjacent_vertices_to.rs index c04fd59..5fe574e 100644 --- a/src/core/vertices/get_full_adjacent_vertices_to.rs +++ b/src/core/vertices/get_full_adjacent_vertices_to.rs @@ -21,7 +21,7 @@ where HE: HyperedgeTrait, { /// Gets the list of all vertices connected to a given vertex as tuples of - /// the form (`VertexIndex`, Vec). + /// the form (`VertexIndex`, `Vec`). pub fn get_full_adjacent_vertices_to( &self, to: VertexIndex, diff --git a/src/core/vertices/get_vertex_hyperedges.rs b/src/core/vertices/get_vertex_hyperedges.rs index f722217..e743f9b 100644 --- a/src/core/vertices/get_vertex_hyperedges.rs +++ b/src/core/vertices/get_vertex_hyperedges.rs @@ -23,9 +23,9 @@ where let (_, hyperedges_index_set) = self .vertices - .get_index(internal_index) + .get(internal_index) .ok_or(HypergraphError::InternalVertexIndexNotFound(internal_index))?; - self.get_hyperedges(&hyperedges_index_set.clone().into_iter().collect_vec()) + self.get_hyperedges(&hyperedges_index_set.iter().copied().collect_vec()) } } diff --git a/src/core/vertices/get_vertex_index.rs b/src/core/vertices/get_vertex_index.rs new file mode 100644 index 0000000..4ebd3b1 --- /dev/null +++ b/src/core/vertices/get_vertex_index.rs @@ -0,0 +1,29 @@ +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the stable indexes of all vertices whose weight equals `weight`. + /// + /// Because vertex weights are not required to be unique, multiple indexes + /// may be returned. Returns an empty `Vec` if no match is found. + /// + /// This is the reverse of [`get_vertex_weight`](Self::get_vertex_weight). + #[must_use] + pub fn get_vertex_index(&self, weight: V) -> Vec { + self.vertices + .iter() + .enumerate() + .filter_map(|(internal, (w, _))| { + (*w == weight).then(|| self.vertices_mapping.left.get(&internal).copied())? + }) + .collect() + } +} diff --git a/src/core/vertices/get_vertex_weight.rs b/src/core/vertices/get_vertex_weight.rs index afa2398..45bb635 100644 --- a/src/core/vertices/get_vertex_weight.rs +++ b/src/core/vertices/get_vertex_weight.rs @@ -19,7 +19,7 @@ where let internal_index = self.get_internal_vertex(vertex_index)?; self.vertices - .get_index(internal_index) + .get(internal_index) .map(|(weight, _)| weight) .ok_or(HypergraphError::InternalVertexIndexNotFound(internal_index)) } diff --git a/src/core/vertices/is_acyclic.rs b/src/core/vertices/is_acyclic.rs new file mode 100644 index 0000000..ec9f0a6 --- /dev/null +++ b/src/core/vertices/is_acyclic.rs @@ -0,0 +1,21 @@ +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexTrait, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns `true` if the hypergraph contains no directed cycles. + /// + /// Implemented as a topological sort; the result is `false` when + /// [`topological_sort`](Self::topological_sort) would return + /// [`HypergraphError::HypergraphContainsCycle`](crate::errors::HypergraphError::HypergraphContainsCycle). + #[must_use] + pub fn is_acyclic(&self) -> bool { + self.topological_sort().is_ok() + } +} diff --git a/src/core/vertices/is_reachable.rs b/src/core/vertices/is_reachable.rs new file mode 100644 index 0000000..0a66d81 --- /dev/null +++ b/src/core/vertices/is_reachable.rs @@ -0,0 +1,54 @@ +use std::collections::VecDeque; + +use ahash::AHashSet; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns `true` if `to` is reachable from `from` via directed hyperedges. + /// + /// A vertex is always considered reachable from itself. + /// + /// Returns [`HypergraphError::VertexIndexNotFound`] if either index does not exist. + pub fn is_reachable( + &self, + from: VertexIndex, + to: VertexIndex, + ) -> Result> { + self.get_internal_vertex(from)?; + self.get_internal_vertex(to)?; + + if from == to { + return Ok(true); + } + + let mut visited: AHashSet = AHashSet::new(); + let mut queue: VecDeque = VecDeque::new(); + + visited.insert(from); + queue.push_back(from); + + while let Some(current) = queue.pop_front() { + for neighbor in self.get_adjacent_vertices_from(current)? { + if neighbor == to { + return Ok(true); + } + if visited.insert(neighbor) { + queue.push_back(neighbor); + } + } + } + + Ok(false) + } +} diff --git a/src/core/vertices/mod.rs b/src/core/vertices/mod.rs index 35e5b6f..484fb1c 100644 --- a/src/core/vertices/mod.rs +++ b/src/core/vertices/mod.rs @@ -5,16 +5,29 @@ pub(crate) mod get_vertex; pub(crate) mod get_vertices; pub mod add_vertex; +pub mod bfs; +pub mod connected_components; +pub mod contains_vertex; pub mod count_vertices; +pub mod dfs; pub mod get_adjacent_vertices_from; pub mod get_adjacent_vertices_to; +pub mod get_all_paths; pub mod get_dijkstra_connections; +pub mod get_dijkstra_from; pub mod get_full_adjacent_vertices_from; pub mod get_full_adjacent_vertices_to; pub mod get_full_vertex_hyperedges; pub mod get_vertex_degree_in; pub mod get_vertex_degree_out; pub mod get_vertex_hyperedges; +pub mod get_vertex_index; pub mod get_vertex_weight; +pub mod is_acyclic; +pub mod is_reachable; pub mod remove_vertex; +pub mod retain_vertices; +pub mod strongly_connected_components; +pub mod subgraph; +pub mod topological_sort; pub mod update_vertex_weight; diff --git a/src/core/vertices/remove_vertex.rs b/src/core/vertices/remove_vertex.rs index e11779a..69489cb 100644 --- a/src/core/vertices/remove_vertex.rs +++ b/src/core/vertices/remove_vertex.rs @@ -1,5 +1,3 @@ -use rayon::prelude::*; - use crate::{ HyperedgeKey, HyperedgeTrait, @@ -38,10 +36,7 @@ where // Get the unique vertices, i.e. check for self-loops. let mut unique_vertices = vertices.clone(); - // We use `par_sort_unstable` here which means that the order of - // equal elements is not preserved but this is fine since we dedupe - // them afterwards. - unique_vertices.par_sort_unstable(); + unique_vertices.sort_unstable(); unique_vertices.dedup(); // Remove the hyperedge if the vertex is the only one present. @@ -51,7 +46,7 @@ where // Otherwise update the hyperedge with the updated vertices. let updated_vertices = self.get_vertices( &vertices - .into_par_iter() + .into_iter() .filter(|vertex| *vertex != internal_index) .collect::>(), )?; @@ -64,7 +59,7 @@ where let last_index = self.vertices.len() - 1; // Swap and remove by index. - self.vertices.swap_remove_index(internal_index); + self.vertices.swap_remove(internal_index); // Update the mapping for the removed vertex. self.vertices_mapping.left.remove(&internal_index); @@ -97,10 +92,10 @@ where .ok_or(HypergraphError::InternalHyperedgeIndexNotFound(hyperedge))?; let updated_vertices = vertices - .into_par_iter() + .iter() .map(|vertex| { // Remap the vertex if this is the swapped one. - if vertex == &last_index { + if *vertex == last_index { internal_index } else { *vertex diff --git a/src/core/vertices/retain_vertices.rs b/src/core/vertices/retain_vertices.rs new file mode 100644 index 0000000..99bb15c --- /dev/null +++ b/src/core/vertices/retain_vertices.rs @@ -0,0 +1,34 @@ +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Retains only the vertices for which `predicate` returns `true`. + /// + /// Removed vertices also drop all hyperedges that contain them. The + /// predicate receives the stable index and a reference to the weight of + /// each vertex. + pub fn retain_vertices(&mut self, mut predicate: F) -> Result<(), HypergraphError> + where + F: FnMut(VertexIndex, &V) -> bool, + { + let to_remove: Vec = self + .vertices_iter() + .filter_map(|(idx, weight)| (!predicate(idx, weight)).then_some(idx)) + .collect(); + + for idx in to_remove { + self.remove_vertex(idx)?; + } + + Ok(()) + } +} diff --git a/src/core/vertices/strongly_connected_components.rs b/src/core/vertices/strongly_connected_components.rs new file mode 100644 index 0000000..602d6b5 --- /dev/null +++ b/src/core/vertices/strongly_connected_components.rs @@ -0,0 +1,92 @@ +use ahash::AHashSet; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the strongly connected components (SCCs) of the hypergraph using + /// Kosaraju's algorithm. + /// + /// Each SCC is a sorted `Vec` of vertices that are mutually + /// reachable following directed hyperedges. A vertex with no edges forms its + /// own single-element SCC. The order of the outer `Vec` follows reverse + /// finish order from the first DFS pass (topological order for DAGs). + /// + /// Returns an empty `Vec` for an empty hypergraph. + pub fn strongly_connected_components( + &self, + ) -> Result>, HypergraphError> { + let mut all_vertices: Vec = + self.vertices_mapping.left.values().copied().collect(); + all_vertices.sort(); + + // Phase 1 — iterative DFS on the original graph; record finish order. + let mut visited: AHashSet = AHashSet::new(); + let mut finish_order: Vec = Vec::new(); + + for &start in &all_vertices { + if visited.contains(&start) { + continue; + } + + // Each stack entry: (vertex, exiting). + // Push (v, false) to enter v, then (v, true) to record finish. + let mut stack: Vec<(VertexIndex, bool)> = vec![(start, false)]; + + while let Some((v, exiting)) = stack.pop() { + if exiting { + finish_order.push(v); + continue; + } + if !visited.insert(v) { + continue; + } + stack.push((v, true)); + for neighbor in self.get_adjacent_vertices_from(v)? { + if !visited.contains(&neighbor) { + stack.push((neighbor, false)); + } + } + } + } + + // Phase 2 — iterative DFS on the transposed graph in reverse finish order. + // The transposed graph is traversed by following incoming edges via + // `get_adjacent_vertices_to`. + let mut visited2: AHashSet = AHashSet::new(); + let mut sccs: Vec> = Vec::new(); + + for &start in finish_order.iter().rev() { + if visited2.contains(&start) { + continue; + } + + let mut scc: Vec = Vec::new(); + let mut stack: Vec = vec![start]; + visited2.insert(start); + + while let Some(v) = stack.pop() { + scc.push(v); + for predecessor in self.get_adjacent_vertices_to(v)? { + if visited2.insert(predecessor) { + stack.push(predecessor); + } + } + } + + scc.sort(); + sccs.push(scc); + } + + Ok(sccs) + } +} diff --git a/src/core/vertices/subgraph.rs b/src/core/vertices/subgraph.rs new file mode 100644 index 0000000..df44629 --- /dev/null +++ b/src/core/vertices/subgraph.rs @@ -0,0 +1,65 @@ +use ahash::{ + AHashMap, + AHashSet, +}; + +use crate::{ + HyperedgeIndex, + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns the induced subgraph for the given vertex set. + /// + /// The subgraph contains exactly the specified vertices (deduplicated and + /// validated) and every hyperedge from the original graph whose vertices are + /// all within that set. Vertex stable indexes are reassigned from `0` in + /// sorted order; hyperedge stable indexes are similarly reassigned in + /// original insertion order. + /// + /// Returns [`HypergraphError::VertexIndexNotFound`] for any invalid index. + pub fn subgraph( + &self, + vertices: &[VertexIndex], + ) -> Result, HypergraphError> { + // Deduplicate and sort for a deterministic, stable output order. + let mut sorted: Vec = vertices.to_vec(); + sorted.sort(); + sorted.dedup(); + + let vertex_set: AHashSet = sorted.iter().copied().collect(); + + let mut sub = Hypergraph::new(); + let mut old_to_new: AHashMap = AHashMap::new(); + + for &old_idx in &sorted { + let weight = self.get_vertex_weight(old_idx)?; + let new_idx = sub.add_vertex(*weight)?; + old_to_new.insert(old_idx, new_idx); + } + + // Include hyperedges whose entire vertex set falls within the subgraph. + let mut hyperedges: Vec<(HyperedgeIndex, &HE)> = self.hyperedges_iter().collect(); + hyperedges.sort_by_key(|(idx, _)| *idx); + + for (he_idx, _) in hyperedges { + let he_vertices = self.get_hyperedge_vertices(he_idx)?; + if he_vertices.iter().all(|v| vertex_set.contains(v)) { + let new_vertices: Vec = + he_vertices.iter().map(|v| old_to_new[v]).collect(); + let he_weight = self.get_hyperedge_weight(he_idx)?; + sub.add_hyperedge(new_vertices, *he_weight)?; + } + } + + Ok(sub) + } +} diff --git a/src/core/vertices/topological_sort.rs b/src/core/vertices/topological_sort.rs new file mode 100644 index 0000000..b96e1b8 --- /dev/null +++ b/src/core/vertices/topological_sort.rs @@ -0,0 +1,70 @@ +use std::{ + cmp::Reverse, + collections::BinaryHeap, +}; + +use ahash::AHashMap; + +use crate::{ + HyperedgeTrait, + Hypergraph, + VertexIndex, + VertexTrait, + errors::HypergraphError, +}; + +impl Hypergraph +where + V: VertexTrait, + HE: HyperedgeTrait, +{ + /// Returns a topological ordering of all vertices using Kahn's algorithm. + /// + /// When multiple vertices are ready at the same step, the one with the + /// smallest [`VertexIndex`] is chosen, giving a deterministic result. + /// + /// Returns [`HypergraphError::HypergraphContainsCycle`] if the hypergraph + /// contains a cycle. + pub fn topological_sort(&self) -> Result, HypergraphError> { + // Collect all stable vertex indexes. + let all_vertices: Vec = self.vertices_mapping.left.values().copied().collect(); + + let vertex_count = all_vertices.len(); + + // Compute in-degree for each vertex (number of directed edges arriving at it). + let mut in_degree: AHashMap = + all_vertices.iter().map(|&v| (v, 0)).collect(); + + for &v in &all_vertices { + for neighbor in self.get_adjacent_vertices_from(v)? { + *in_degree.entry(neighbor).or_insert(0) += 1; + } + } + + // Seed the min-heap with all zero-in-degree vertices. + let mut heap: BinaryHeap> = in_degree + .iter() + .filter_map(|(&v, °)| (deg == 0).then_some(Reverse(v))) + .collect(); + + let mut result: Vec = Vec::with_capacity(vertex_count); + + while let Some(Reverse(current)) = heap.pop() { + result.push(current); + + for neighbor in self.get_adjacent_vertices_from(current)? { + let deg = in_degree.entry(neighbor).or_insert(0); + *deg -= 1; + if *deg == 0 { + heap.push(Reverse(neighbor)); + } + } + } + + if result.len() == vertex_count { + Ok(result) + } else { + Err(HypergraphError::HypergraphContainsCycle) + } + } +} diff --git a/src/core/vertices/update_vertex_weight.rs b/src/core/vertices/update_vertex_weight.rs index 079f6ea..7facefd 100644 --- a/src/core/vertices/update_vertex_weight.rs +++ b/src/core/vertices/update_vertex_weight.rs @@ -12,6 +12,10 @@ where HE: HyperedgeTrait, { /// Updates the weight of a vertex by index. + /// + /// The new weight need not be unique — multiple vertices may carry the same + /// weight value. Returns [`HypergraphError::VertexWeightUnchanged`] if + /// `weight` equals the current weight (no-op guard). pub fn update_vertex_weight( &mut self, vertex_index: VertexIndex, @@ -21,11 +25,10 @@ where let (previous_weight, index_set) = self .vertices - .get_index(internal_index) - .map(|(previous_weight, index_set)| (previous_weight.to_owned(), index_set.clone())) + .get(internal_index) + .map(|(w, s)| (*w, s.clone())) .ok_or(HypergraphError::InternalVertexIndexNotFound(internal_index))?; - // Return an error if the new weight is the same as the previous one. if weight == previous_weight { return Err(HypergraphError::VertexWeightUnchanged { index: vertex_index, @@ -33,28 +36,12 @@ where }); } - // Return an error if the new weight is already assigned to another - // vertex. - if self.vertices.contains_key(&weight) { - return Err(HypergraphError::VertexWeightAlreadyAssigned(weight)); - } - - // We can't directly replace the value in the map. - // First, we need to insert the new weight, it will end up - // being at the last position. - // Since we have already checked that the new weight is not in the - // map, we can safely perform the operation without checking its output. - self.vertices.insert(weight, index_set); - - // Then we use swap and remove. This will remove the previous weight - // and insert the new one at the index position of the former. - // This doesn't alter the indexing. - // See the update_hyperedge_weight method for more detailed explanation. - // Since we know that the internal index is correct, we can safely - // perform the operation without checking its output. - self.vertices.swap_remove_index(internal_index); + // Append the new entry at the end, then swap-remove the old position so + // that the new entry lands at `internal_index`. The BiHashMap mapping is + // unchanged — `internal_index` still maps to `vertex_index`. + self.vertices.push((weight, index_set)); + self.vertices.swap_remove(internal_index); - // Return a unit. Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 8ae1534..fe3371c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,20 +6,26 @@ //! //! This library enables you to: //! -//! - represent **non-simple** hypergraphs with two or more hyperedges - with different weights - containing the exact same set of vertices -//! - represent **self-loops** - i.e., hyperedges containing vertices directed to themselves one or more times -//! - represent **unaries** - i.e., hyperedges containing a unique vertex +//! - represent **non-simple** hypergraphs with two or more hyperedges containing the exact same set of vertices +//! - represent **self-loops** — i.e., hyperedges containing vertices directed to themselves one or more times +//! - represent **unaries** — i.e., hyperedges containing a unique vertex //! //! Additional features: //! //! - Safe Rust implementation //! - Proper error handling -//! - Stable indexes assigned for each hyperedge and each vertex +//! - Stable indexes for each hyperedge and each vertex — identity is the index, not the weight; duplicate weights are permitted on both sides +//! - Graph traversal: BFS, DFS, topological sort, reachability check, all simple paths +//! - Shortest paths: Dijkstra point-to-point and single-source +//! - Structural analysis: strongly connected components, weakly connected components, subgraph extraction, cycle detection +//! - Filtered views: `retain_vertices`, `retain_hyperedges` +//! - Optional **`serde`** feature for serialization/deserialization support +//! (enable with `features = ["serde"]` in `Cargo.toml`) //! //! ## Example //! //! Please notice that the hyperedges and the vertices must implement the -//! [`HyperedgeTrait`](crate::HyperedgeTrait) and the [`VertexTrait`](crate::VertexTrait) respectively. +//! [`HyperedgeTrait`] and the [`VertexTrait`] respectively. //! //! ``` //! use hypergraph::{HyperedgeIndex, Hypergraph, VertexIndex}; @@ -39,7 +45,7 @@ //! //! impl<'a> Display for Person<'a> { //! fn fmt(&self, f: &mut Formatter<'_>) -> Result { -//! write!(f, "{}", self) +//! write!(f, "{}", self.name) //! } //! } //! @@ -58,13 +64,13 @@ //! //! impl<'a> Display for Relation<'a> { //! fn fmt(&self, f: &mut Formatter<'_>) -> Result { -//! write!(f, "{}", self) +//! write!(f, "{}", self.name) //! } //! } //! -//! impl<'a> Into for Relation<'a> { -//! fn into(self) -> usize { -//! self.cost +//! impl<'a> From> for usize { +//! fn from(relation: Relation<'a>) -> usize { +//! relation.cost //! } //! } //! diff --git a/tests/integration_dijkstra.rs b/tests/integration_dijkstra.rs index 77b7e93..029a1f3 100644 --- a/tests/integration_dijkstra.rs +++ b/tests/integration_dijkstra.rs @@ -71,3 +71,62 @@ fn integration_dijkstra() { "should follow a, b, c, e, d with their matching traversed hyperedges" ); } + +#[test] +fn integration_dijkstra_parallel_hyperedges() { + // Regression test: with multiple hyperedges between the same two vertices, + // Dijkstra must pick the cheapest one, not merely the first. + let mut graph = Hypergraph::::new(); + + let s = graph.add_vertex(Vertex::new("s")).unwrap(); + let t = graph.add_vertex(Vertex::new("t")).unwrap(); + + // Insert the expensive hyperedge first so it appears first in iteration order. + let _expensive = graph + .add_hyperedge(vec![s, t], Hyperedge::new("expensive", 10)) + .unwrap(); + let cheap = graph + .add_hyperedge(vec![s, t], Hyperedge::new("cheap", 1)) + .unwrap(); + + assert_eq!( + graph.get_dijkstra_connections(s, t), + Ok(vec![(s, None), (t, Some(cheap))]), + "should use the cheaper parallel hyperedge" + ); +} + +#[test] +fn integration_dijkstra_dead_end_branch() { + // Regression test: vertices explored in dead-end branches must not appear + // in the returned path. + let mut graph = Hypergraph::::new(); + + let s = graph.add_vertex(Vertex::new("s")).unwrap(); + let a = graph.add_vertex(Vertex::new("a")).unwrap(); // dead-end branch + let b = graph.add_vertex(Vertex::new("b")).unwrap(); + let c = graph.add_vertex(Vertex::new("c")).unwrap(); // unreachable from b to t + let t = graph.add_vertex(Vertex::new("t")).unwrap(); + + // s→a is cheap but a only leads to c, which is a dead end. + let _he_sa = graph + .add_hyperedge(vec![s, a], Hyperedge::new("s-a", 1)) + .unwrap(); + let _he_ac = graph + .add_hyperedge(vec![a, c], Hyperedge::new("a-c", 1)) + .unwrap(); + + // s→b→t is the only route to t. + let he_sb = graph + .add_hyperedge(vec![s, b], Hyperedge::new("s-b", 2)) + .unwrap(); + let he_bt = graph + .add_hyperedge(vec![b, t], Hyperedge::new("b-t", 1)) + .unwrap(); + + assert_eq!( + graph.get_dijkstra_connections(s, t), + Ok(vec![(s, None), (b, Some(he_sb)), (t, Some(he_bt))]), + "dead-end vertex a should not appear in the path" + ); +} diff --git a/tests/integration_main.rs b/tests/integration_main.rs index a636106..bd305f9 100644 --- a/tests/integration_main.rs +++ b/tests/integration_main.rs @@ -51,12 +51,6 @@ fn integration_main() { Ok(VertexIndex(4)), "should add the fifth vertex" ); - assert_eq!( - graph.add_vertex(enola), - Err(HypergraphError::VertexWeightAlreadyAssigned(enola)), - "should return an explicit error since this weight is already in use" - ); - // Count the vertices. assert_eq!(graph.count_vertices(), 5, "should have 5 vertices"); @@ -118,10 +112,8 @@ fn integration_main() { ); assert_eq!( graph.add_hyperedge(vec![VertexIndex(3)], fourth_hyperedge,), - Err(HypergraphError::HyperedgeWeightAlreadyAssigned( - fourth_hyperedge - )), - "should return an explicit error since this weight is already in use" + Ok(HyperedgeIndex(3)), + "should return the existing index since the (vertices, weight) key is identical" ); assert_eq!( graph.add_hyperedge(vec![VertexIndex(3)], fifth_hyperedge), @@ -586,10 +578,8 @@ fn integration_main() { ); assert_eq!( graph.update_hyperedge_weight(HyperedgeIndex(0), fourth_hyperedge), - Err(HypergraphError::HyperedgeWeightAlreadyAssigned( - fourth_hyperedge - )), - "should return an explicit error since this weight is already assigned" + Ok(()), + "should update the weight to fourth_hyperedge even though another hyperedge uses it" ); // Update the vertices of some hyperedges. diff --git a/tests/integration_new_features.rs b/tests/integration_new_features.rs new file mode 100644 index 0000000..d3e2de3 --- /dev/null +++ b/tests/integration_new_features.rs @@ -0,0 +1,801 @@ +//! Integration tests for features added in the modernization pass: +//! is_empty, Clone, vertices_iter, hyperedges_iter, IntoIterator for &Hypergraph, +//! BFS, DFS, is_reachable, topological_sort, get_dijkstra_connections_with_cost, +//! contains_vertex, get_vertex_index, is_acyclic, Display, connected_components, +//! get_dijkstra_from. + +#![deny(unsafe_code, nonstandard_style)] +#![allow(missing_docs)] + +mod common; + +use common::{ + Hyperedge, + Vertex, +}; +use hypergraph::{ + HyperedgeIndex, + Hypergraph, + VertexIndex, +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn build_graph<'a>() -> ( + Hypergraph, Hyperedge<'a>>, + VertexIndex, + VertexIndex, + VertexIndex, + VertexIndex, + HyperedgeIndex, + HyperedgeIndex, + HyperedgeIndex, +) { + let mut g = Hypergraph::::new(); + + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + let c = g.add_vertex(Vertex::new("c")).unwrap(); + let d = g.add_vertex(Vertex::new("d")).unwrap(); + + // a → b (cost 1), b → c (cost 2), a → c (cost 5) + let ab = g + .add_hyperedge(vec![a, b], Hyperedge::new("a-b", 1)) + .unwrap(); + let bc = g + .add_hyperedge(vec![b, c], Hyperedge::new("b-c", 2)) + .unwrap(); + let ac = g + .add_hyperedge(vec![a, c], Hyperedge::new("a-c", 5)) + .unwrap(); + + (g, a, b, c, d, ab, bc, ac) +} + +// --------------------------------------------------------------------------- +// is_empty +// --------------------------------------------------------------------------- + +#[test] +fn is_empty_on_new_graph() { + let g = Hypergraph::::new(); + assert!(g.is_empty()); +} + +#[test] +fn is_empty_after_adding_vertex() { + let mut g = Hypergraph::::new(); + g.add_vertex(Vertex::new("x")).unwrap(); + assert!(!g.is_empty()); +} + +#[test] +fn is_empty_after_clear() { + let (mut g, ..) = build_graph(); + g.clear(); + assert!(g.is_empty()); +} + +// --------------------------------------------------------------------------- +// Clone +// --------------------------------------------------------------------------- + +#[test] +fn clone_produces_independent_copy() { + let (g, a, _, _, _, _, _, _) = build_graph(); + let mut g2 = g.clone(); + + assert_eq!(g.count_vertices(), g2.count_vertices()); + assert_eq!(g.count_hyperedges(), g2.count_hyperedges()); + + // Mutating the clone must not affect the original. + g2.remove_vertex(a).unwrap(); + assert_eq!(g.count_vertices(), 4); + assert_eq!(g2.count_vertices(), 3); +} + +// --------------------------------------------------------------------------- +// vertices_iter / hyperedges_iter +// --------------------------------------------------------------------------- + +#[test] +fn vertices_iter_yields_all() { + let (g, a, b, c, d, ..) = build_graph(); + let mut collected: Vec = g.vertices_iter().map(|(idx, _)| idx).collect(); + collected.sort(); + assert_eq!(collected, vec![a, b, c, d]); +} + +#[test] +fn hyperedges_iter_yields_all() { + let (g, _, _, _, _, ab, bc, ac) = build_graph(); + let mut collected: Vec = g.hyperedges_iter().map(|(idx, _)| idx).collect(); + collected.sort(); + assert_eq!(collected, vec![ab, bc, ac]); +} + +// --------------------------------------------------------------------------- +// IntoIterator for &Hypergraph +// --------------------------------------------------------------------------- + +#[test] +fn ref_into_iterator() { + let (g, ..) = build_graph(); + let count = (&g).into_iter().count(); + assert_eq!(count, g.count_hyperedges()); +} + +// --------------------------------------------------------------------------- +// BFS +// --------------------------------------------------------------------------- + +#[test] +fn bfs_from_source() { + let (g, a, b, c, _, ..) = build_graph(); + let bfs = g.get_bfs(a).unwrap(); + // a must come first; b and c must both appear. + assert_eq!(bfs[0], a); + assert!(bfs.contains(&b)); + assert!(bfs.contains(&c)); +} + +#[test] +fn bfs_isolated_vertex() { + let (g, _, _, _, d, ..) = build_graph(); + // d has no outgoing edges in the test graph. + assert_eq!(g.get_bfs(d).unwrap(), vec![d]); +} + +#[test] +fn bfs_invalid_vertex() { + let (g, ..) = build_graph(); + assert!(g.get_bfs(VertexIndex(999)).is_err()); +} + +// --------------------------------------------------------------------------- +// DFS +// --------------------------------------------------------------------------- + +#[test] +fn dfs_from_source() { + let (g, a, b, c, _, ..) = build_graph(); + let dfs = g.get_dfs(a).unwrap(); + assert_eq!(dfs[0], a); + assert!(dfs.contains(&b)); + assert!(dfs.contains(&c)); +} + +#[test] +fn dfs_isolated_vertex() { + let (g, _, _, _, d, ..) = build_graph(); + assert_eq!(g.get_dfs(d).unwrap(), vec![d]); +} + +#[test] +fn dfs_invalid_vertex() { + let (g, ..) = build_graph(); + assert!(g.get_dfs(VertexIndex(999)).is_err()); +} + +// --------------------------------------------------------------------------- +// is_reachable +// --------------------------------------------------------------------------- + +#[test] +fn is_reachable_self() { + let (g, a, ..) = build_graph(); + assert_eq!(g.is_reachable(a, a).unwrap(), true); +} + +#[test] +fn is_reachable_direct_edge() { + let (g, a, b, ..) = build_graph(); + assert_eq!(g.is_reachable(a, b).unwrap(), true); +} + +#[test] +fn is_reachable_transitive() { + let (g, a, _, c, ..) = build_graph(); + assert_eq!(g.is_reachable(a, c).unwrap(), true); +} + +#[test] +fn is_reachable_false() { + let (g, a, _, _, d, ..) = build_graph(); + // d has no incoming edges, only a has outgoing ones. + assert_eq!(g.is_reachable(d, a).unwrap(), false); +} + +#[test] +fn is_reachable_invalid_vertex() { + let (g, a, ..) = build_graph(); + assert!(g.is_reachable(a, VertexIndex(999)).is_err()); + assert!(g.is_reachable(VertexIndex(999), a).is_err()); +} + +// --------------------------------------------------------------------------- +// topological_sort +// --------------------------------------------------------------------------- + +#[test] +fn topological_sort_dag() { + let (g, a, b, c, d, ..) = build_graph(); + let order = g.topological_sort().unwrap(); + + // In a valid topological order a must appear before b and c. + let pos = |v: VertexIndex| order.iter().position(|&x| x == v).unwrap(); + + assert!(pos(a) < pos(b), "a must precede b"); + assert!(pos(a) < pos(c), "a must precede c"); + assert!(pos(b) < pos(c), "b must precede c"); + // d is isolated — just verify it appears somewhere. + assert!(order.contains(&d)); + assert_eq!(order.len(), 4); +} + +#[test] +fn topological_sort_cyclic() { + let mut g = Hypergraph::::new(); + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + g.add_hyperedge(vec![a, b], Hyperedge::new("a-b", 1)) + .unwrap(); + g.add_hyperedge(vec![b, a], Hyperedge::new("b-a", 1)) + .unwrap(); + + assert!(g.topological_sort().is_err()); +} + +// --------------------------------------------------------------------------- +// get_dijkstra_connections_with_cost +// --------------------------------------------------------------------------- + +#[test] +fn dijkstra_with_cost_finds_shortest() { + let (g, a, b, c, _, ab, bc, _ac) = build_graph(); + // Shortest a→c is via a→b→c with cost 3, not direct a→c with cost 5. + let (cost, path) = g.get_dijkstra_connections_with_cost(a, c).unwrap(); + assert_eq!(cost, 3); + assert_eq!(path, vec![(a, None), (b, Some(ab)), (c, Some(bc))]); +} + +#[test] +fn dijkstra_with_cost_matches_without_cost() { + let (g, a, _, c, ..) = build_graph(); + let (_, path_with) = g.get_dijkstra_connections_with_cost(a, c).unwrap(); + let path_without = g.get_dijkstra_connections(a, c).unwrap(); + assert_eq!(path_with, path_without); +} + +#[test] +fn dijkstra_with_cost_no_path() { + let (g, _, _, _, d, ..) = build_graph(); + let a = VertexIndex(0); + // d has no outgoing edges, so d→a has no path. + let (cost, path) = g.get_dijkstra_connections_with_cost(d, a).unwrap(); + assert_eq!(cost, 0); + assert!(path.is_empty()); +} + +// --------------------------------------------------------------------------- +// contains_vertex / get_vertex_index +// --------------------------------------------------------------------------- + +#[test] +fn contains_vertex_present() { + let (g, ..) = build_graph(); + assert!(g.contains_vertex(Vertex::new("a"))); + assert!(g.contains_vertex(Vertex::new("b"))); +} + +#[test] +fn contains_vertex_absent() { + let (g, ..) = build_graph(); + assert!(!g.contains_vertex(Vertex::new("z"))); +} + +#[test] +fn get_vertex_index_roundtrip() { + let (g, a, b, c, d, ..) = build_graph(); + assert_eq!(g.get_vertex_index(Vertex::new("a")), vec![a]); + assert_eq!(g.get_vertex_index(Vertex::new("b")), vec![b]); + assert_eq!(g.get_vertex_index(Vertex::new("c")), vec![c]); + assert_eq!(g.get_vertex_index(Vertex::new("d")), vec![d]); +} + +#[test] +fn get_vertex_index_absent() { + let (g, ..) = build_graph(); + assert!(g.get_vertex_index(Vertex::new("z")).is_empty()); +} + +#[test] +fn get_vertex_index_after_remove() { + let (mut g, a, ..) = build_graph(); + g.remove_vertex(a).unwrap(); + assert!(g.get_vertex_index(Vertex::new("a")).is_empty()); +} + +#[test] +fn get_vertex_index_duplicate_weights() { + // Two vertices with the same weight are now permitted. + let mut g = Hypergraph::::new(); + let x1 = g.add_vertex(Vertex::new("x")).unwrap(); + let x2 = g.add_vertex(Vertex::new("x")).unwrap(); + assert_ne!(x1, x2); + let mut found = g.get_vertex_index(Vertex::new("x")); + found.sort(); + assert_eq!(found, vec![x1, x2]); +} + +// --------------------------------------------------------------------------- +// is_acyclic +// --------------------------------------------------------------------------- + +#[test] +fn is_acyclic_dag() { + let (g, ..) = build_graph(); + assert!(g.is_acyclic()); +} + +#[test] +fn is_acyclic_with_cycle() { + let mut g = Hypergraph::::new(); + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + g.add_hyperedge(vec![a, b], Hyperedge::new("a-b", 1)) + .unwrap(); + g.add_hyperedge(vec![b, a], Hyperedge::new("b-a", 1)) + .unwrap(); + assert!(!g.is_acyclic()); +} + +#[test] +fn is_acyclic_empty() { + let g = Hypergraph::::new(); + assert!(g.is_acyclic()); +} + +// --------------------------------------------------------------------------- +// Display +// --------------------------------------------------------------------------- + +#[test] +fn display_contains_vertex_weights() { + let (g, ..) = build_graph(); + let s = g.to_string(); + assert!(s.contains("a"), "missing vertex a in: {s}"); + assert!(s.contains("b"), "missing vertex b in: {s}"); + assert!(s.contains("c"), "missing vertex c in: {s}"); + assert!(s.contains("d"), "missing vertex d in: {s}"); +} + +#[test] +fn display_contains_hyperedge_weights() { + let (g, ..) = build_graph(); + let s = g.to_string(); + assert!(s.contains("a-b"), "missing hyperedge a-b in: {s}"); + assert!(s.contains("b-c"), "missing hyperedge b-c in: {s}"); +} + +#[test] +fn display_empty_graph() { + let g = Hypergraph::::new(); + let s = g.to_string(); + assert!(s.contains("vertices: []"), "unexpected: {s}"); + assert!(s.contains("hyperedges: []"), "unexpected: {s}"); +} + +// --------------------------------------------------------------------------- +// connected_components +// --------------------------------------------------------------------------- + +#[test] +fn connected_components_single() { + // a → b → c, all in one weakly-connected component; d is isolated. + let (g, a, b, c, d, ..) = build_graph(); + let mut components = g.connected_components().unwrap(); + // Sort for determinism (already sorted by implementation, but be explicit). + for comp in &mut components { + comp.sort(); + } + components.sort_by_key(|c| c[0]); + + assert_eq!(components.len(), 2, "expected 2 components"); + // The connected component contains a, b, c. + assert!(components[0].contains(&a)); + assert!(components[0].contains(&b)); + assert!(components[0].contains(&c)); + // d is isolated. + assert_eq!(components[1], vec![d]); +} + +#[test] +fn connected_components_all_isolated() { + let mut g = Hypergraph::::new(); + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + let components = g.connected_components().unwrap(); + assert_eq!(components.len(), 2); + assert!(components.iter().any(|c| c == &vec![a])); + assert!(components.iter().any(|c| c == &vec![b])); +} + +#[test] +fn connected_components_empty_graph() { + let g = Hypergraph::::new(); + assert_eq!( + g.connected_components().unwrap(), + Vec::>::new() + ); +} + +// --------------------------------------------------------------------------- +// get_dijkstra_from +// --------------------------------------------------------------------------- + +#[test] +fn dijkstra_from_includes_source() { + let (g, a, ..) = build_graph(); + let dists = g.get_dijkstra_from(a).unwrap(); + assert_eq!(dists[&a], 0); +} + +#[test] +fn dijkstra_from_correct_distances() { + let (g, a, b, c, ..) = build_graph(); + // a→b cost 1, a→c cost 5, b→c cost 2 (so a→b→c = 3 < 5) + let dists = g.get_dijkstra_from(a).unwrap(); + assert_eq!(dists[&a], 0); + assert_eq!(dists[&b], 1); + assert_eq!(dists[&c], 3); +} + +#[test] +fn dijkstra_from_excludes_unreachable() { + let (g, _, _, _, d, ..) = build_graph(); + // d has no outgoing edges. + let dists = g.get_dijkstra_from(d).unwrap(); + assert_eq!(dists.len(), 1); + assert_eq!(dists[&d], 0); +} + +#[test] +fn dijkstra_from_consistent_with_point_to_point() { + let (g, a, _, c, ..) = build_graph(); + let (cost, _) = g.get_dijkstra_connections_with_cost(a, c).unwrap(); + let dists = g.get_dijkstra_from(a).unwrap(); + assert_eq!(dists[&c], cost); +} + +#[test] +fn dijkstra_from_invalid_vertex() { + let (g, ..) = build_graph(); + assert!(g.get_dijkstra_from(VertexIndex(999)).is_err()); +} + +// --------------------------------------------------------------------------- +// find_hyperedges_by_weight +// --------------------------------------------------------------------------- + +#[test] +fn find_hyperedges_by_weight_single_match() { + let (g, _, _, _, _, ab, _, _) = build_graph(); + assert_eq!( + g.find_hyperedges_by_weight(Hyperedge::new("a-b", 1)), + vec![ab] + ); +} + +#[test] +fn find_hyperedges_by_weight_no_match() { + let (g, ..) = build_graph(); + assert!( + g.find_hyperedges_by_weight(Hyperedge::new("zzz", 99)) + .is_empty() + ); +} + +#[test] +fn find_hyperedges_by_weight_duplicate_weights() { + // Two hyperedges with the same weight on different vertex sets. + let mut g = Hypergraph::::new(); + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + let c = g.add_vertex(Vertex::new("c")).unwrap(); + + let shared = Hyperedge::new("shared", 7); + let he1 = g.add_hyperedge(vec![a, b], shared).unwrap(); + let he2 = g.add_hyperedge(vec![b, c], shared).unwrap(); + + let mut found = g.find_hyperedges_by_weight(shared); + found.sort(); + assert_eq!(found, vec![he1, he2]); +} + +// --------------------------------------------------------------------------- +// strongly_connected_components +// --------------------------------------------------------------------------- + +#[test] +fn scc_dag_all_singletons() { + // In a DAG every vertex is its own SCC. + let (g, a, b, c, d, ..) = build_graph(); + let sccs = g.strongly_connected_components().unwrap(); + assert_eq!(sccs.len(), 4); + // Each vertex appears in exactly one singleton SCC. + for v in [a, b, c, d] { + assert!( + sccs.iter().any(|s| s == &vec![v]), + "expected singleton SCC for {:?}", + v + ); + } +} + +#[test] +fn scc_cycle_single_component() { + let mut g = Hypergraph::::new(); + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + let c = g.add_vertex(Vertex::new("c")).unwrap(); + g.add_hyperedge(vec![a, b], Hyperedge::new("a-b", 1)) + .unwrap(); + g.add_hyperedge(vec![b, c], Hyperedge::new("b-c", 1)) + .unwrap(); + g.add_hyperedge(vec![c, a], Hyperedge::new("c-a", 1)) + .unwrap(); + + let sccs = g.strongly_connected_components().unwrap(); + assert_eq!(sccs.len(), 1); + let mut scc = sccs.into_iter().next().unwrap(); + scc.sort(); + assert_eq!(scc, vec![a, b, c]); +} + +#[test] +fn scc_mixed_structure() { + // a ⇄ b (mutual cycle), c → a (c reaches the cycle but is not in it). + let mut g = Hypergraph::::new(); + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + let c = g.add_vertex(Vertex::new("c")).unwrap(); + g.add_hyperedge(vec![a, b], Hyperedge::new("a-b", 1)) + .unwrap(); + g.add_hyperedge(vec![b, a], Hyperedge::new("b-a", 1)) + .unwrap(); + g.add_hyperedge(vec![c, a], Hyperedge::new("c-a", 1)) + .unwrap(); + + let sccs = g.strongly_connected_components().unwrap(); + assert_eq!(sccs.len(), 2); + assert!( + sccs.iter().any(|s| s == &vec![c]), + "c should be a singleton SCC" + ); + assert!( + sccs.iter().any(|s| { + let mut s = s.clone(); + s.sort(); + s == vec![a, b] + }), + "a and b should form one SCC" + ); +} + +#[test] +fn scc_empty_graph() { + let g = Hypergraph::::new(); + assert_eq!( + g.strongly_connected_components().unwrap(), + Vec::>::new() + ); +} + +// --------------------------------------------------------------------------- +// subgraph +// --------------------------------------------------------------------------- + +#[test] +fn subgraph_preserves_internal_edges() { + let (g, a, b, _, _, ab, _, _) = build_graph(); + // Subgraph on {a, b}: should include the a→b hyperedge. + let sub = g.subgraph(&[a, b]).unwrap(); + assert_eq!(sub.count_vertices(), 2); + assert_eq!(sub.count_hyperedges(), 1); + // The only hyperedge should have weight "a-b". + let (he_idx, he_weight) = sub.hyperedges_iter().next().unwrap(); + assert_eq!(*he_weight, Hyperedge::new("a-b", 1)); + let _ = (he_idx, ab); // suppress unused warning +} + +#[test] +fn subgraph_drops_crossing_edges() { + // Subgraph on {b, d}: neither b→c (c not in set) nor a→b (a not in set) + // should appear; a→c is also excluded. + let (g, _, b, _, d, ..) = build_graph(); + let sub = g.subgraph(&[b, d]).unwrap(); + assert_eq!(sub.count_vertices(), 2); + assert_eq!(sub.count_hyperedges(), 0); +} + +#[test] +fn subgraph_empty_vertex_list() { + let (g, ..) = build_graph(); + let sub = g.subgraph(&[]).unwrap(); + assert!(sub.is_empty()); +} + +#[test] +fn subgraph_full_vertex_set() { + let (g, a, b, c, d, ..) = build_graph(); + let sub = g.subgraph(&[a, b, c, d]).unwrap(); + assert_eq!(sub.count_vertices(), g.count_vertices()); + assert_eq!(sub.count_hyperedges(), g.count_hyperedges()); +} + +#[test] +fn subgraph_deduplicates_vertices() { + let (g, a, b, ..) = build_graph(); + let sub = g.subgraph(&[a, b, a, b]).unwrap(); + assert_eq!(sub.count_vertices(), 2); +} + +#[test] +fn subgraph_invalid_vertex() { + let (g, ..) = build_graph(); + assert!(g.subgraph(&[VertexIndex(999)]).is_err()); +} + +// --------------------------------------------------------------------------- +// retain_vertices +// --------------------------------------------------------------------------- + +#[test] +fn retain_vertices_keep_all() { + let (mut g, ..) = build_graph(); + g.retain_vertices(|_, _| true).unwrap(); + assert_eq!(g.count_vertices(), 4); + assert_eq!(g.count_hyperedges(), 3); +} + +#[test] +fn retain_vertices_remove_all() { + let (mut g, ..) = build_graph(); + g.retain_vertices(|_, _| false).unwrap(); + assert!(g.is_empty()); +} + +#[test] +fn retain_vertices_by_index() { + // Keep only vertices with index < 2 (i.e. a and b); removing c and d. + let (mut g, a, b, ..) = build_graph(); + g.retain_vertices(|idx, _| idx.0 < 2).unwrap(); + assert_eq!(g.count_vertices(), 2); + assert!(g.contains_vertex(Vertex::new("a"))); + assert!(g.contains_vertex(Vertex::new("b"))); + // remove_vertex strips c from multi-vertex hyperedges rather than deleting + // them: ab=[a,b] is unchanged, bc=[b] and ac=[a] shrink to unary. + assert_eq!(g.count_hyperedges(), 3); + // The a→b hyperedge still contains both a and b. + let ab_idx = g + .find_hyperedges_by_weight(Hyperedge::new("a-b", 1)) + .into_iter() + .next() + .unwrap(); + let ab_verts = g.get_hyperedge_vertices(ab_idx).unwrap(); + assert!(ab_verts.contains(&a)); + assert!(ab_verts.contains(&b)); +} + +#[test] +fn retain_vertices_strips_vertex_from_hyperedges() { + // Removing vertex c strips it from bc and ac rather than deleting them. + let (mut g, ..) = build_graph(); + g.retain_vertices(|_, weight| *weight != Vertex::new("c")) + .unwrap(); + assert_eq!(g.count_vertices(), 3); + // bc=[b], ac=[a], ab=[a,b] — all three hyperedges survive. + assert_eq!(g.count_hyperedges(), 3); +} + +// --------------------------------------------------------------------------- +// retain_hyperedges +// --------------------------------------------------------------------------- + +#[test] +fn retain_hyperedges_keep_all() { + let (mut g, ..) = build_graph(); + g.retain_hyperedges(|_, _| true).unwrap(); + assert_eq!(g.count_hyperedges(), 3); + assert_eq!(g.count_vertices(), 4); +} + +#[test] +fn retain_hyperedges_remove_all() { + let (mut g, ..) = build_graph(); + g.retain_hyperedges(|_, _| false).unwrap(); + assert_eq!(g.count_hyperedges(), 0); + assert_eq!(g.count_vertices(), 4); // vertices unaffected +} + +#[test] +fn retain_hyperedges_by_cost() { + // Keep only hyperedges with cost <= 2 (a→b cost 1, b→c cost 2; drop a→c cost 5). + let (mut g, _, _, _, _, ab, bc, _) = build_graph(); + g.retain_hyperedges(|_, w| usize::from(*w) <= 2).unwrap(); + assert_eq!(g.count_hyperedges(), 2); + let mut remaining: Vec<_> = g.hyperedges_iter().map(|(idx, _)| idx).collect(); + remaining.sort(); + assert!(remaining.contains(&ab)); + assert!(remaining.contains(&bc)); +} + +// --------------------------------------------------------------------------- +// get_all_paths +// --------------------------------------------------------------------------- + +#[test] +fn all_paths_single_route() { + // Only path from b to c is b→c directly. + let (g, _, b, c, ..) = build_graph(); + let paths = g.get_all_paths(b, c).unwrap(); + assert_eq!(paths.len(), 1); + assert_eq!(paths[0], vec![b, c]); +} + +#[test] +fn all_paths_multiple_routes() { + // From a to c: a→b→c (cost 3) and a→c directly. + let (g, a, b, c, ..) = build_graph(); + let mut paths = g.get_all_paths(a, c).unwrap(); + paths.sort(); + assert_eq!(paths.len(), 2); + assert!(paths.contains(&vec![a, b, c])); + assert!(paths.contains(&vec![a, c])); +} + +#[test] +fn all_paths_no_route() { + // d has no outgoing edges. + let (g, a, _, _, d, ..) = build_graph(); + assert_eq!( + g.get_all_paths(d, a).unwrap(), + Vec::>::new() + ); +} + +#[test] +fn all_paths_from_equals_to() { + let (g, a, ..) = build_graph(); + assert_eq!(g.get_all_paths(a, a).unwrap(), vec![vec![a]]); +} + +#[test] +fn all_paths_no_cycle_infinite_loop() { + // Graph with a cycle should not loop forever; paths that revisit a node + // are pruned (simple paths only). + let mut g = Hypergraph::::new(); + let a = g.add_vertex(Vertex::new("a")).unwrap(); + let b = g.add_vertex(Vertex::new("b")).unwrap(); + let c = g.add_vertex(Vertex::new("c")).unwrap(); + g.add_hyperedge(vec![a, b], Hyperedge::new("a-b", 1)) + .unwrap(); + g.add_hyperedge(vec![b, a], Hyperedge::new("b-a", 1)) + .unwrap(); + g.add_hyperedge(vec![b, c], Hyperedge::new("b-c", 1)) + .unwrap(); + + // Only simple path from a to c is a→b→c. + let paths = g.get_all_paths(a, c).unwrap(); + assert_eq!(paths, vec![vec![a, b, c]]); +} + +#[test] +fn all_paths_invalid_vertex() { + let (g, a, ..) = build_graph(); + assert!(g.get_all_paths(VertexIndex(999), a).is_err()); + assert!(g.get_all_paths(a, VertexIndex(999)).is_err()); +} diff --git a/tests/integration_serde.rs b/tests/integration_serde.rs new file mode 100644 index 0000000..29df4d7 --- /dev/null +++ b/tests/integration_serde.rs @@ -0,0 +1,89 @@ +//! Integration tests for the optional `serde` feature. + +#![deny(unsafe_code, nonstandard_style)] +#![allow(missing_docs)] + +#[cfg(feature = "serde")] +mod serde_tests { + use std::fmt::{ + Display, + Formatter, + Result, + }; + + use hypergraph::{ + HyperedgeIndex, + Hypergraph, + VertexIndex, + }; + + // Owned types that can derive serde traits. + + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] + struct V(u32); + + impl Display for V { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "{}", self.0) + } + } + + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] + struct HE(u32); + + impl Display for HE { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "{}", self.0) + } + } + + impl From for usize { + fn from(HE(cost): HE) -> Self { + cost as usize + } + } + + fn build() -> (Hypergraph, VertexIndex, VertexIndex, HyperedgeIndex) { + let mut g = Hypergraph::::new(); + let a = g.add_vertex(V(0)).unwrap(); + let b = g.add_vertex(V(1)).unwrap(); + let ab = g.add_hyperedge(vec![a, b], HE(10)).unwrap(); + (g, a, b, ab) + } + + #[test] + fn round_trip_json() { + let (g, a, b, ab) = build(); + + let json = serde_json::to_string(&g).expect("serialize"); + let g2: Hypergraph = serde_json::from_str(&json).expect("deserialize"); + + assert_eq!(g2.count_vertices(), 2); + assert_eq!(g2.count_hyperedges(), 1); + assert_eq!(g2.get_vertex_weight(a).unwrap(), &V(0)); + assert_eq!(g2.get_vertex_weight(b).unwrap(), &V(1)); + assert_eq!(g2.get_hyperedge_weight(ab).unwrap(), &HE(10)); + assert_eq!(g2.get_hyperedge_vertices(ab).unwrap(), vec![a, b]); + } + + #[test] + fn empty_graph_round_trip() { + let g = Hypergraph::::new(); + let json = serde_json::to_string(&g).expect("serialize"); + let g2: Hypergraph = serde_json::from_str(&json).expect("deserialize"); + assert!(g2.is_empty()); + } + + #[test] + fn indexes_serialize() { + let idx = VertexIndex(42); + let json = serde_json::to_string(&idx).unwrap(); + let back: VertexIndex = serde_json::from_str(&json).unwrap(); + assert_eq!(back, idx); + + let hidx = HyperedgeIndex(7); + let json = serde_json::to_string(&hidx).unwrap(); + let back: HyperedgeIndex = serde_json::from_str(&json).unwrap(); + assert_eq!(back, hidx); + } +}