diff --git a/Cargo.lock b/Cargo.lock index c7573fb..0aa4bc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -93,6 +104,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "binary" version = "0.1.0" @@ -105,6 +122,34 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -158,6 +203,27 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "castaway" version = "0.2.3" @@ -184,6 +250,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -227,6 +302,36 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "4.6.7" @@ -274,6 +379,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -347,7 +458,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags", + "bitflags 2.6.0", "crossterm_winapi", "libc", "parking_lot", @@ -426,6 +537,18 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", ] [[package]] @@ -538,6 +661,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -557,7 +689,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -592,6 +724,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbag" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f494b2060b2a8f5e63379e1e487258e014cee1b1725a735816c0107a2e9d93" + [[package]] name = "hashbrown" version = "0.12.3" @@ -637,6 +775,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -669,12 +816,151 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -703,6 +989,15 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "into-attr" version = "0.1.1" @@ -812,6 +1107,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lexical-core" version = "1.0.2" @@ -882,6 +1189,16 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.8" @@ -905,6 +1222,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -964,6 +1287,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -995,6 +1324,16 @@ dependencies = [ "target-features", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "now" version = "0.1.3" @@ -1076,6 +1415,35 @@ dependencies = [ "regex", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1247,7 +1615,7 @@ dependencies = [ "streaming-iterator", "strength_reduce", "version_check", - "zstd", + "zstd 0.13.2", ] [[package]] @@ -1283,7 +1651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5bc2cadcca904a9dc4d2c2b437c346712806e9a678bf17c7e94ebf622faae76" dependencies = [ "ahash", - "bitflags", + "bitflags 2.6.0", "bytemuck", "chrono", "chrono-tz", @@ -1327,7 +1695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34e9c0e8c7ba93aac64051b92dc68eac5a0e9543cf44ca784467db2c035821fe" dependencies = [ "ahash", - "bitflags", + "bitflags 2.6.0", "once_cell", "polars-arrow", "polars-compute", @@ -1404,7 +1772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e61c062e833d2376de0a4cf745504449215cbf499cea293cb592e674ffb39ca" dependencies = [ "ahash", - "bitflags", + "bitflags 2.6.0", "memchr", "once_cell", "polars-arrow", @@ -1528,7 +1896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3f728df4bc643492a2057a0a125c7e550cbcfe35b391444653ad294be9ab190" dependencies = [ "ahash", - "bitflags", + "bitflags 2.6.0", "bytemuck", "bytes", "chrono", @@ -1679,10 +2047,12 @@ dependencies = [ "chrono", "flate2", "graphviz-rust", + "hashbag", "polars", "quick-xml", "rayon", "rusqlite", + "russcip", "serde", "serde_json", "serde_with", @@ -1867,7 +2237,7 @@ version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1916,7 +2286,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1968,13 +2338,28 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rusqlite" version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags", + "bitflags 2.6.0", "chrono", "fallible-iterator", "fallible-streaming-iterator", @@ -1983,19 +2368,66 @@ dependencies = [ "smallvec", ] +[[package]] +name = "russcip" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa6cc9df880e7f532d098010ac2f9e1db4b2a03046516c1ea99e7cf4cf13b2f" +dependencies = [ + "scip-sys", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -2017,6 +2449,22 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scip-sys" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5699e26a9a6a2a233e7f3163f5ccacc996b7cb42c7bed9bc3aef0f06c6de803" +dependencies = [ + "bindgen", + "cc", + "cmake", + "glob", + "tempfile", + "ureq", + "zip 0.5.13", + "zip-extract", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2082,7 +2530,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_with_macros", - "time", + "time 0.3.36", ] [[package]] @@ -2097,6 +2545,17 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2150,6 +2609,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "sqlparser" version = "0.49.0" @@ -2159,6 +2624,12 @@ dependencies = [ "log", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "stacker" version = "0.1.17" @@ -2224,6 +2695,12 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -2246,6 +2723,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "sysinfo" version = "0.31.4" @@ -2304,6 +2792,17 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.36" @@ -2335,6 +2834,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "typenum" version = "1.17.0" @@ -2380,6 +2889,54 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30e6f97efe1fa43535ee241ee76967d3ff6ff3953ebb430d8d55c5393029e7b" +dependencies = [ + "base64", + "flate2", + "litemap", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots", + "yoke", + "zerofrom", +] + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.10.0" @@ -2424,6 +2981,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2495,6 +3058,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2736,12 +3320,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "xxhash-rust" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -2763,13 +3383,125 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time 0.1.45", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zip-extract" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e109e5a291403b4c1e514d39f8a22d3f98d257e691a52bb1f16051bb1ffed63e" +dependencies = [ + "log", + "thiserror", + "zip 0.6.6", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + [[package]] name = "zstd" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe", + "zstd-safe 7.2.1", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", ] [[package]] diff --git a/process_mining/Cargo.toml b/process_mining/Cargo.toml index cde0aca..f39f154 100644 --- a/process_mining/Cargo.toml +++ b/process_mining/Cargo.toml @@ -25,6 +25,8 @@ flate2 = "1.0" graphviz-rust = { git = "https://github.com/aarkue/graphviz-rust.git" , optional = true} rusqlite = { version = "0.32.1", features = ["bundled","chrono", "serialize"], optional = true } polars = {version = "0.43.1", features = ["dtype-slim","timezones","partition_by"], optional = true} +hashbag = "0.1.12" +russcip = { version = "0.4.1", features = ["bundled"] } [features] diff --git a/process_mining/src/lib.rs b/process_mining/src/lib.rs index a888b71..4f6018c 100644 --- a/process_mining/src/lib.rs +++ b/process_mining/src/lib.rs @@ -49,6 +49,9 @@ pub mod event_log { /// Util module with smaller helper functions, structs or enums pub mod utils; +pub mod oc_petri_net; +pub mod oc_case; +pub mod oc_align; /// /// Petri nets diff --git a/process_mining/src/oc_align/align_case.rs b/process_mining/src/oc_align/align_case.rs new file mode 100644 index 0000000..cb8a9b0 --- /dev/null +++ b/process_mining/src/oc_align/align_case.rs @@ -0,0 +1,655 @@ +// Assuming the provided context code is in a module named `oc_case` +use crate::oc_case::case::{CaseGraph, Edge, EdgeType, Event, Node, Object}; +use russcip::prelude::*; +use russcip::Variable; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; + +trait Mappable { + fn is_void(&self) -> bool; + fn cost(&self) -> f64; +} + +#[derive(Debug, Clone)] +enum NodeMapping { + RealNode(usize, usize), // (c1_node, c2_node) + VoidNode(usize, usize), // (c1_node, void_node_id) +} + +#[derive(Debug, Clone)] +enum EdgeMapping { + RealEdge(usize, usize), // (c1_edge, c2_edge) + VoidEdge(usize, usize), // (c1_edge, void_edge_id) +} + +impl Mappable for NodeMapping { + fn is_void(&self) -> bool { + matches!(self, NodeMapping::VoidNode(_, _)) + } + fn cost(&self) -> f64 { + match self { + NodeMapping::RealNode(_, _) => 0.0, + NodeMapping::VoidNode(_, _) => 1.0, + } + } +} + +impl Mappable for EdgeMapping { + fn is_void(&self) -> bool { + matches!(self, EdgeMapping::VoidEdge(_, _)) + } + fn cost(&self) -> f64 { + match self { + EdgeMapping::RealEdge(_, _) => 0.0, + EdgeMapping::VoidEdge(_, _) => 1.0, + } + } +} + +#[derive(Debug, Clone)] +pub struct CaseAlignment<'a> { + pub c1: &'a CaseGraph, + pub c2: &'a CaseGraph, + pub void_nodes: HashMap, // id -> Node + pub void_edges: HashMap, // id -> Edge + pub node_mapping: HashMap, // c1_node_id -> mapping + pub edge_mapping: HashMap, // c1_edge_id -> mapping +} + +impl<'a> CaseAlignment<'a> { + pub fn align_mip(c1: &'a CaseGraph, c2: &'a CaseGraph) -> Self { + let mut model = Model::new() + .hide_output() + .include_default_plugins() + .create_prob("CaseAlignment") + .set_obj_sense(ObjSense::Minimize); + + // Variables + // Node mapping variables: x_{i,j} = 1 if c1 node i maps to c2 node j + let mut x_vars: HashMap<(usize, usize), Rc> = HashMap::new(); + for n1 in c1.nodes.keys() { + for n2 in c2.nodes.keys() { + // Only map nodes of the same type + let node1 = c1.nodes.get(n1).unwrap(); + let node2 = c2.nodes.get(n2).unwrap(); + if nodes_compatible(node1, node2) { + let var = + model.add_var(0., 1., 0., &format!("x_{}_{}", n1, n2), VarType::Binary); + x_vars.insert((*n1, *n2), var); + } + } + // Option to map to a void node + let var = model.add_var(0., 1., 1., &format!("x_void_{}", n1), VarType::Binary); + x_vars.insert((*n1, 0), var); // Using 0 to denote void + } + + // Each node in c1 must be mapped to exactly one node in c2 or to a void node + for n1 in c1.nodes.keys() { + let mut vars = Vec::new(); + let mut coeffs = Vec::new(); + for n2 in c2.nodes.keys() { + if let Some(v) = x_vars.get(&(*n1, *n2)) { + vars.push(v.clone()); + coeffs.push(1.0); + } + } + if let Some(v) = x_vars.get(&(*n1, 0)) { + vars.push(v.clone()); + coeffs.push(1.0); + } + model.add_cons(vars, &coeffs, 1.0, 1.0, &format!("map_node_{}", n1)); + } + + // Each node in c2 can be mapped to at most one node in c1 + for n2 in c2.nodes.keys() { + let mut vars = Vec::new(); + let mut coeffs = Vec::new(); + for n1 in c1.nodes.keys() { + if let Some(v) = x_vars.get(&(*n1, *n2)) { + vars.push(v.clone()); + coeffs.push(1.0); + } + } + model.add_cons(vars, &coeffs, 0.0, 1.0, &format!("c2_node_once_{}", n2)); + } + + // Create edge mapping variables similarly + let mut y_vars: HashMap<(usize, usize), Rc> = HashMap::new(); + for e1 in c1.edges.keys() { + for e2 in c2.edges.keys() { + let edge1 = c1.edges.get(e1).unwrap(); + let edge2 = c2.edges.get(e2).unwrap(); + if edges_compatible(edge1, edge2) { + let var = + model.add_var(0., 1., 0., &format!("y_{}_{}", e1, e2), VarType::Binary); + y_vars.insert((*e1, *e2), var); + } + } + // Option to map to a void edge + let var = model.add_var(0., 1., 1., &format!("y_void_{}", e1), VarType::Binary); + y_vars.insert((*e1, 0), var); // Using 0 to denote void + } + + // Each edge in c1 must be mapped to exactly one edge in c2 or to a void edge + for e1 in c1.edges.keys() { + let mut vars = Vec::new(); + let mut coeffs = Vec::new(); + for e2 in c2.edges.keys() { + if let Some(v) = y_vars.get(&(*e1, *e2)) { + vars.push(v.clone()); + coeffs.push(1.0); + } + } + if let Some(v) = y_vars.get(&(*e1, 0)) { + vars.push(v.clone()); + coeffs.push(1.0); + } + model.add_cons(vars, &coeffs, 1.0, 1.0, &format!("map_edge_{}", e1)); + } + + // Each edge in c2 can be mapped to at most one edge in c1 + for e2 in c2.edges.keys() { + let mut vars = Vec::new(); + let mut coeffs = Vec::new(); + for e1 in c1.edges.keys() { + if let Some(v) = y_vars.get(&(*e1, *e2)) { + vars.push(v.clone()); + coeffs.push(1.0); + } + } + model.add_cons(vars, &coeffs, 0.0, 1.0, &format!("c2_edge_once_{}", e2)); + } + + // Structure preservation: if two nodes are mapped, their edges should correspond + for (&e1_id, e1) in &c1.edges { + let from1 = e1.from; + let to1 = e1.to; + for (&e2_id, e2) in &c2.edges { + // If e1 maps to e2, then from1 maps to e2.from and to1 maps to e2.to + if let (Some(x_from), Some(x_to)) = + (x_vars.get(&(from1, e2.from)), x_vars.get(&(to1, e2.to))) + { + if let Some(y_var) = y_vars.get(&(e1_id, e2_id)) { + // y >= x_from + x_to -1 + model.add_cons( + vec![y_var.clone(), x_from.clone(), x_to.clone()], + &[1.0, -1.0, -1.0], + -f64::INFINITY, + 0.0, + &format!("struct_pres_e{}_e{}", e1_id, e2_id), + ); + } + } + } + } + + // Objective: minimize number of void nodes and void edges plus unused nodes and edges in c2 + let mut obj_vars = Vec::new(); + let mut obj_coeffs = Vec::new(); + + // Void nodes + for n1 in c1.nodes.keys() { + if let Some(v) = x_vars.get(&(*n1, 0)) { + obj_vars.push(v.clone()); + obj_coeffs.push(1.0); + } + } + + // Void edges + for e1 in c1.edges.keys() { + if let Some(v) = y_vars.get(&(*e1, 0)) { + obj_vars.push(v.clone()); + obj_coeffs.push(1.0); + } + } + + // Unused nodes in c2 + for n2 in c2.nodes.keys() { + let var = model.add_var(0., 1., 1., &format!("unused_node_{}", n2), VarType::Binary); + // If unused_node is 1, then no x_{i,j} can be 1 for this n2 + for n1 in c1.nodes.keys() { + if let Some(x_var) = x_vars.get(&(*n1, *n2)) { + model.add_cons( + vec![x_var.clone(), var.clone()], + &[1.0, 1.0], + -f64::INFINITY, + 1.0, + &format!("unused_node_def_{}", n2), + ); + } + } + obj_vars.push(var.clone()); + obj_coeffs.push(1.0); + + let mut sum_vars = Vec::new(); + let mut sum_coeffs = Vec::new(); + for n1 in c1.nodes.keys() { + if let Some(x_var) = x_vars.get(&(*n1, *n2)) { + sum_vars.push(x_var.clone()); + sum_coeffs.push(1.0); + } + } + sum_vars.push(var.clone()); + sum_coeffs.push(1.0); + model.add_cons( + sum_vars, + &sum_coeffs, + 1.0, // Lower bound + 1.0, // Upper bound + &format!("unused_node_eq_{}", n2), + ); + } + + // Unused edges in c2 + for e2 in c2.edges.keys() { + let var = model.add_var(0., 1., 1., &format!("unused_edge_{}", e2), VarType::Binary); + // If unused_edge is 1, then no y_{i,j} can be 1 for this e2 + for e1 in c1.edges.keys() { + if let Some(y_var) = y_vars.get(&(*e1, *e2)) { + model.add_cons( + vec![y_var.clone(), var.clone()], + &[1.0, 1.0], + -f64::INFINITY, + 1.0, + &format!("unused_edge_def_{}", e2), + ); + } + } + obj_vars.push(var.clone()); + obj_coeffs.push(1.0); + + let mut sum_vars = Vec::new(); + let mut sum_coeffs = Vec::new(); + for e1 in c1.edges.keys() { + if let Some(y_var) = y_vars.get(&(*e1, *e2)) { + sum_vars.push(y_var.clone()); + sum_coeffs.push(1.0); + } + } + sum_vars.push(var.clone()); + sum_coeffs.push(1.0); + model.add_cons( + sum_vars, + &sum_coeffs, + 1.0, // Lower bound + 1.0, // Upper bound + &format!("unused_edge_eq_{}", e2), + ); + } + + //model.set_obj(&obj_vars, &obj_coeffs); + + // Solve the model + let solved_model = model.solve(); + + let status = solved_model.status(); + println!("Solved with status {:?}", status); + + if solved_model.status() != Status::Optimal { + panic!("No optimal solution found"); + } + + let obj_val = solved_model.obj_val(); + println!("Objective value: {}", obj_val); + + let sol = solved_model.best_sol().unwrap(); + + // Extract node mappings + let mut node_mapping = HashMap::new(); + for (&(n1, n2), var) in &x_vars { + if sol.val(var.clone()) > 0.5 { + if n2 == 0 { + // Mapped to void + node_mapping.insert( + n1, + NodeMapping::VoidNode(n1, n1), // Using n1 as void id + ); + } else { + node_mapping.insert(n1, NodeMapping::RealNode(n1, n2)); + } + } + } + + // Extract edge mappings + let mut edge_mapping = HashMap::new(); + for (&(e1, e2), var) in &y_vars { + if sol.val(var.clone()) > 0.5 { + if e2 == 0 { + // Mapped to void + edge_mapping.insert( + e1, + EdgeMapping::VoidEdge(e1, e1), // Using e1 as void id + ); + } else { + edge_mapping.insert(e1, EdgeMapping::RealEdge(e1, e2)); + } + } + } + + // Collect void nodes and edges + let mut void_nodes = HashMap::new(); + for (&n1, mapping) in &node_mapping { + if mapping.is_void() { + let node = c1.nodes.get(&n1).unwrap().clone(); + void_nodes.insert(n1, node); + } + } + + let mut void_edges = HashMap::new(); + for (&e1, mapping) in &edge_mapping { + if mapping.is_void() { + let edge = c1.edges.get(&e1).unwrap().clone(); + void_edges.insert(e1, edge); + } + } + + CaseAlignment { + c1, + c2, + void_nodes, + void_edges, + node_mapping, + edge_mapping, + } + } + + /// Computes the total cost of the alignment. + /// + /// Returns: + /// - Ok(total_cost) if the alignment is valid. + /// - Err(error_message) if the alignment is invalid. + pub fn total_cost(&self) -> Result { + // 1. Validate that all nodes in c1 are mapped + if self.node_mapping.len() != self.c1.nodes.len() { + return Err("Not all nodes in c1 are mapped.".to_string()); + } + + // 2. Validate that all edges in c1 are mapped + if self.edge_mapping.len() != self.c1.edges.len() { + return Err("Not all edges in c1 are mapped.".to_string()); + } + + // 3. Ensure no node in c2 is mapped more than once + let mut mapped_c2_nodes: HashSet = HashSet::new(); + for mapping in self.node_mapping.values() { + if let NodeMapping::RealNode(_, c2_node_id) = mapping { + if !mapped_c2_nodes.insert(*c2_node_id) { + return Err(format!( + "Node in c2 with ID {} is mapped more than once.", + c2_node_id + )); + } + } + } + + // 4. Ensure no edge in c2 is mapped more than once + let mut mapped_c2_edges: HashSet = HashSet::new(); + for mapping in self.edge_mapping.values() { + if let EdgeMapping::RealEdge(_, c2_edge_id) = mapping { + if !mapped_c2_edges.insert(*c2_edge_id) { + return Err(format!( + "Edge in c2 with ID {} is mapped more than once.", + c2_edge_id + )); + } + } + } + + // 5. Calculate the total cost + let mut total = 0.0; + + // 5a. Sum the costs of all node mappings + for mapping in self.node_mapping.values() { + total += mapping.cost(); + } + + // 5b. Sum the costs of all edge mappings + for mapping in self.edge_mapping.values() { + total += mapping.cost(); + } + + // 5c. Add cost for unmapped nodes in c2 + let mapped_c2_nodes_count = mapped_c2_nodes.len(); + let total_c2_nodes = self.c2.nodes.len(); + let unmapped_c2_nodes = (total_c2_nodes as isize - mapped_c2_nodes_count as isize).max(0) as f64; + total += unmapped_c2_nodes; + + // 5d. Add cost for unmapped edges in c2 + let mapped_c2_edges_count = mapped_c2_edges.len(); + let total_c2_edges = self.c2.edges.len(); + let unmapped_c2_edges = (total_c2_edges as isize - mapped_c2_edges_count as isize).max(0) as f64; + total += unmapped_c2_edges; + + Ok(total) + } + + /// Prints the mappings of the alignment in a readable format. + /// + /// This includes: + /// - How each node and edge in c1 is mapped to c2 or to a void. + /// - Any nodes and edges in c2 that are not mapped. + /// - Any nodes and edges in c1 that are not mapped (if alignment is invalid). + fn print_mappings(&self) { + println!("=== Node Mappings ==="); + // Track mapped c2 node IDs to identify unmapped nodes in c2 later + let mut mapped_c2_nodes: HashSet = HashSet::new(); + + for (c1_node_id, mapping) in &self.node_mapping { + match mapping { + NodeMapping::RealNode(_, c2_node_id) => { + println!(" c1 Node {} -> c2 Node {}", c1_node_id, c2_node_id); + mapped_c2_nodes.insert(*c2_node_id); + } + NodeMapping::VoidNode(_, void_node_id) => { + println!( + " c1 Node {} -> VOID Node (Void ID: {})", + c1_node_id, void_node_id + ); + } + } + } + + // Identify and print any unmapped nodes in c1 (shouldn't exist if alignment is valid) + let mapped_c1_nodes: HashSet<&usize> = self.node_mapping.keys().collect(); + let unmapped_c1_nodes: Vec<_> = self + .c1 + .nodes + .keys() + .filter(|id| !mapped_c1_nodes.contains(id)) + .collect(); + + if !unmapped_c1_nodes.is_empty() { + println!("\n--- Unmapped c1 Nodes (Invalid Alignment) ---"); + for c1_node_id in unmapped_c1_nodes { + println!(" c1 Node {} -> NOT MAPPED", c1_node_id); + } + } + + println!("\n--- Unmapped c2 Nodes ---"); + let unmapped_c2_nodes: Vec<_> = self + .c2 + .nodes + .keys() + .filter(|id| !mapped_c2_nodes.contains(id)) + .collect(); + + if unmapped_c2_nodes.is_empty() { + println!(" All c2 nodes are mapped."); + } else { + for c2_node_id in unmapped_c2_nodes { + println!(" c2 Node {} is UNMAPPED", c2_node_id); + } + } + + println!("\n=== Edge Mappings ==="); + // Track mapped c2 edge IDs to identify unmapped edges in c2 later + let mut mapped_c2_edges: HashSet = HashSet::new(); + + for (c1_edge_id, mapping) in &self.edge_mapping { + match mapping { + EdgeMapping::RealEdge(_, c2_edge_id) => { + println!(" c1 Edge {} -> c2 Edge {}", c1_edge_id, c2_edge_id); + mapped_c2_edges.insert(*c2_edge_id); + } + EdgeMapping::VoidEdge(_, void_edge_id) => { + println!( + " c1 Edge {} -> VOID Edge (Void ID: {})", + c1_edge_id, void_edge_id + ); + } + } + } + + // Identify and print any unmapped edges in c1 (shouldn't exist if alignment is valid) + let mapped_c1_edges: HashSet<&usize> = self.edge_mapping.keys().collect(); + let unmapped_c1_edges: Vec<_> = self + .c1 + .edges + .keys() + .filter(|id| !mapped_c1_edges.contains(id)) + .collect(); + + if !unmapped_c1_edges.is_empty() { + println!("\n--- Unmapped c1 Edges (Invalid Alignment) ---"); + for c1_edge_id in unmapped_c1_edges { + println!(" c1 Edge {} -> NOT MAPPED", c1_edge_id); + } + } + + println!("\n--- Unmapped c2 Edges ---"); + let unmapped_c2_edges: Vec<_> = self + .c2 + .edges + .keys() + .filter(|id| !mapped_c2_edges.contains(id)) + .collect(); + + if unmapped_c2_edges.is_empty() { + println!(" All c2 edges are mapped."); + } else { + for c2_edge_id in unmapped_c2_edges { + println!(" c2 Edge {} is UNMAPPED", c2_edge_id); + } + } + + // Optionally, print void nodes and edges details + if !self.void_nodes.is_empty() { + println!("\n--- Void Nodes in Alignment ---"); + for (void_id, node) in &self.void_nodes { + println!(" Void Node ID {}: {:?}", void_id, node); + } + } + + if !self.void_edges.is_empty() { + println!("\n--- Void Edges in Alignment ---"); + for (void_id, edge) in &self.void_edges { + println!(" Void Edge ID {}: {:?}", void_id, edge); + } + } + } +} + +// Helper functions to check compatibility +fn nodes_compatible(n1: &Node, n2: &Node) -> bool { + match (n1, n2) { + (Node::Event(e1), Node::Event(e2)) => e1.event_type == e2.event_type, + (Node::Object(o1), Node::Object(o2)) => o1.object_type == o2.object_type, + _ => false, + } +} + +fn edges_compatible(e1: &Edge, e2: &Edge) -> bool { + e1.edge_type == e2.edge_type +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_align() { + // Example usage with test graphs + let mut c1 = CaseGraph::new(); + let mut c2 = CaseGraph::new(); + + // Populate c1 + let event1 = Node::Event(Event { + id: 1, + event_type: "A".to_string(), + }); + let event2 = Node::Event(Event { + id: 2, + event_type: "B".to_string(), + }); + let object1 = Node::Object(Object { + id: 3, + object_type: "Person".to_string(), + }); + c1.add_node(event1); + c1.add_node(event2); + c1.add_node(object1); + c1.add_edge(Edge::new(1, 1, 2, EdgeType::DF)); + c1.add_edge(Edge::new(2, 2, 3, EdgeType::E2O)); + + // Populate c2 + let event3 = Node::Event(Event { + id: 4, + event_type: "A".to_string(), + }); + let event4 = Node::Event(Event { + id: 5, + event_type: "B".to_string(), + }); + let object2 = Node::Object(Object { + id: 6, + object_type: "Person".to_string(), + }); + let object3 = Node::Object(Object { + id: 7, + object_type: "Device".to_string(), + }); + c2.add_node(event3); + c2.add_node(event4); + c2.add_node(object2); + c2.add_node(object3); + c2.add_edge(Edge::new(101, 4, 5, EdgeType::DF)); + c2.add_edge(Edge::new(102, 5, 6, EdgeType::E2O)); + c2.add_edge(Edge::new(103, 5, 7, EdgeType::E2O)); + + // Align using MIP + let alignment = CaseAlignment::align_mip(&c1, &c2); + + // Print the alignment + println!("Node Mappings:"); + for (&n1, mapping) in &alignment.node_mapping { + match mapping { + NodeMapping::RealNode(_, n2) => { + println!("c1 Node {} -> c2 Node {}", n1, n2); + } + NodeMapping::VoidNode(_, _) => { + println!("c1 Node {} -> Void", n1); + } + } + } + + println!("\nEdge Mappings:"); + for (&e1, mapping) in &alignment.edge_mapping { + match mapping { + EdgeMapping::RealEdge(_, e2) => { + println!("c1 Edge {} -> c2 Edge {}", e1, e2); + } + EdgeMapping::VoidEdge(_, _) => { + println!("c1 Edge {} -> Void", e1); + } + } + } + + + // output the alignment total cost to console, if the alignment is valid else print the error message + match alignment.total_cost() { + Ok(cost) => println!("Total cost: {}", cost), + Err(err) => println!("Error: {}", err), + } + + alignment.print_mappings(); + } +} diff --git a/process_mining/src/oc_align/align_case_model.rs b/process_mining/src/oc_align/align_case_model.rs new file mode 100644 index 0000000..e69de29 diff --git a/process_mining/src/oc_align/mod.rs b/process_mining/src/oc_align/mod.rs new file mode 100644 index 0000000..93ed7e0 --- /dev/null +++ b/process_mining/src/oc_align/mod.rs @@ -0,0 +1,2 @@ +mod align_case; +mod align_case_model; \ No newline at end of file diff --git a/process_mining/src/oc_case/case.rs b/process_mining/src/oc_case/case.rs new file mode 100644 index 0000000..4b9408a --- /dev/null +++ b/process_mining/src/oc_case/case.rs @@ -0,0 +1,256 @@ +use crate::id_based_impls; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; + +// Define the Event struct +#[derive(Debug, Clone)] +pub struct Event { + pub id: usize, + pub event_type: String, +} +id_based_impls!(Event); + +// Define the Object struct +#[derive(Debug, Clone)] +pub struct Object { + pub id: usize, + pub object_type: String, +} +id_based_impls!(Object); + +// Define the Node enum which can be either an Event or an Object +#[derive(Debug, Clone)] +pub enum Node { + Event(Event), + Object(Object), +} + +impl Node { + pub fn id(&self) -> usize { + match self { + Node::Event(event) => event.id, + Node::Object(object) => object.id, + } + } +} + +impl PartialEq for Node { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for Node {} + +impl Hash for Node { + fn hash(&self, state: &mut H) { + self.id().hash(state); + } +} + +// Define the EdgeType enum +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EdgeType { + DF, // Event to Event + O2O, // Object to Object + E2O, // Event to Object +} + +// Define the Edge struct with additional attributes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Edge { + pub id: usize, + pub from: usize, + pub to: usize, + pub edge_type: EdgeType, + // Additional attributes can be added here + // For example: + // weight: f64, + // label: String, +} + +impl Edge { + pub fn new(id: usize, from: usize, to: usize, edge_type: EdgeType) -> Self { + Edge { + id, + from, + to, + edge_type, + // Initialize additional attributes here + } + } +} + +// Define the CaseGraph structure +#[derive(Debug, Clone)] +pub struct CaseGraph { + pub nodes: HashMap, // Keyed by node ID + pub edges: HashMap, // Keyed by edge ID + adjacency: HashMap>, // from node ID -> Vec of edge IDs + id_to_index: HashMap, // Map from node ID to index (if needed) +} + +impl CaseGraph { + pub fn new() -> Self { + CaseGraph { + nodes: HashMap::new(), + edges: HashMap::new(), + adjacency: HashMap::new(), + id_to_index: HashMap::new(), + } + } + + // Add a node to the graph + pub fn add_node(&mut self, node: Node) { + let id = node.id(); + self.id_to_index.insert(id, self.nodes.len()); + self.nodes.insert(id, node); + } + + // Add an edge to the graph with additional attributes + pub fn add_edge(&mut self, edge: Edge) { + let edge_id = edge.id; + let from = edge.from; + self.edges.insert(edge_id, edge); + self.adjacency.entry(from).or_insert_with(Vec::new).push(edge_id); + } + + // Retrieve node by id + pub fn get_node(&self, id: usize) -> Option<&Node> { + self.nodes.get(&id) + } + + // Retrieve edge by id + pub fn get_edge(&self, id: usize) -> Option<&Edge> { + self.edges.get(&id) + } + + // Retrieve outgoing edges from a node + pub fn get_outgoing_edges(&self, from: usize) -> Option<&Vec> { + self.adjacency.get(&from) + } + + // Retrieve neighbors by edge type + pub fn get_neighbors_by_edge_type(&self, from: usize, edge_type: EdgeType) -> Vec { + match self.adjacency.get(&from) { + Some(edge_ids) => edge_ids.iter() + .filter_map(|eid| { + self.edges.get(eid).and_then(|edge| { + if edge.edge_type == edge_type { + Some(edge.to) + } else { + None + } + }) + }) + .collect(), + None => Vec::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_nodes() { + let mut graph = CaseGraph::new(); + let event1 = Event { id: 1, event_type: "A".to_string() }; + let object1 = Object { id: 2, object_type: "Person".to_string() }; + graph.add_node(Node::Event(event1.clone())); + graph.add_node(Node::Object(object1.clone())); + assert_eq!(graph.nodes.len(), 2); + assert_eq!(graph.get_node(1), Some(&Node::Event(event1))); + assert_eq!(graph.get_node(2), Some(&Node::Object(object1))); + } + + #[test] + fn test_add_edges() { + let mut graph = CaseGraph::new(); + // Add nodes + let event1 = Event { id: 1, event_type: "A".to_string() }; + let event2 = Event { id: 2, event_type: "B".to_string() }; + let object1 = Object { id: 3, object_type: "Person".to_string() }; + let object2 = Object { id: 4, object_type: "Device".to_string() }; + graph.add_node(Node::Event(event1)); + graph.add_node(Node::Event(event2)); + graph.add_node(Node::Object(object1)); + graph.add_node(Node::Object(object2)); + // Add edges + let edge1 = Edge::new(1, 1, 2, EdgeType::DF); // Event1 -> Event2 + let edge2 = Edge::new(2, 3, 4, EdgeType::O2O); // Object1 -> Object2 + let edge3 = Edge::new(3, 2, 3, EdgeType::E2O); // Event2 -> Object1 + graph.add_edge(edge1); + graph.add_edge(edge2); + graph.add_edge(edge3); + // Verify DF edge + let df_neighbors = graph.get_neighbors_by_edge_type(1, EdgeType::DF); + assert_eq!(df_neighbors.len(), 1); + assert_eq!(df_neighbors[0], 2); + // Verify O2O edge + let o2o_neighbors = graph.get_neighbors_by_edge_type(3, EdgeType::O2O); + assert_eq!(o2o_neighbors.len(), 1); + assert_eq!(o2o_neighbors[0], 4); + // Verify E2O edge + let e2o_neighbors = graph.get_neighbors_by_edge_type(2, EdgeType::E2O); + assert_eq!(e2o_neighbors.len(), 1); + assert_eq!(e2o_neighbors[0], 3); + } + + #[test] + fn test_get_neighbors_empty() { + let graph = CaseGraph::new(); + // Attempt to get neighbors from an empty graph + assert!(graph.get_neighbors_by_edge_type(1, EdgeType::DF).is_empty()); + assert!(graph.get_neighbors_by_edge_type(2, EdgeType::O2O).is_empty()); + assert!(graph.get_neighbors_by_edge_type(3, EdgeType::E2O).is_empty()); + } + + #[test] + fn test_duplicate_edges() { + let mut graph = CaseGraph::new(); + // Add nodes + let event1 = Event { id: 1, event_type: "A".to_string() }; + let event2 = Event { id: 2, event_type: "B".to_string() }; + graph.add_node(Node::Event(event1)); + graph.add_node(Node::Event(event2)); + // Add duplicate DF edges + let edge1 = Edge::new(1, 1, 2, EdgeType::DF); + let edge2 = Edge::new(2, 1, 2, EdgeType::DF); + graph.add_edge(edge1); + graph.add_edge(edge2); + let df_neighbors = graph.get_neighbors_by_edge_type(1, EdgeType::DF); + assert_eq!(df_neighbors.len(), 2); + assert_eq!(df_neighbors[0], 2); + assert_eq!(df_neighbors[1], 2); + } + + #[test] + fn test_multiple_edge_types() { + let mut graph = CaseGraph::new(); + // Add nodes + let event1 = Event { id: 1, event_type: "A".to_string() }; + let event2 = Event { id: 2, event_type: "B".to_string() }; + let object1 = Object { id: 3, object_type: "Person".to_string() }; + graph.add_node(Node::Event(event1)); + graph.add_node(Node::Event(event2)); + graph.add_node(Node::Object(object1)); + // Add different types of edges from event1 + let edge1 = Edge::new(1, 1, 2, EdgeType::DF); // DF edge + let edge2 = Edge::new(2, 1, 3, EdgeType::E2O); // E2O edge + graph.add_edge(edge1); + graph.add_edge(edge2); + // Verify DF edge + let df_neighbors = graph.get_neighbors_by_edge_type(1, EdgeType::DF); + assert_eq!(df_neighbors.len(), 1); + assert_eq!(df_neighbors[0], 2); + // Verify E2O edge + let e2o_neighbors = graph.get_neighbors_by_edge_type(1, EdgeType::E2O); + assert_eq!(e2o_neighbors.len(), 1); + assert_eq!(e2o_neighbors[0], 3); + // Verify no O2O edges + let o2o_neighbors = graph.get_neighbors_by_edge_type(1, EdgeType::O2O); + assert!(o2o_neighbors.is_empty()); + } +} \ No newline at end of file diff --git a/process_mining/src/oc_case/mod.rs b/process_mining/src/oc_case/mod.rs new file mode 100644 index 0000000..f63fadc --- /dev/null +++ b/process_mining/src/oc_case/mod.rs @@ -0,0 +1 @@ +pub mod case; \ No newline at end of file diff --git a/process_mining/src/oc_petri_net/marking.rs b/process_mining/src/oc_petri_net/marking.rs new file mode 100644 index 0000000..e983971 --- /dev/null +++ b/process_mining/src/oc_petri_net/marking.rs @@ -0,0 +1,251 @@ +use crate::oc_petri_net::oc_petri_net::{InputArc, ObjectCentricPetriNet, Transition}; +use crate::oc_petri_net::util::intersect_hashbag::intersect_hashbags; +use hashbag::HashBag; +use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use uuid::Uuid; +static COUNTER: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct OCToken { + id: usize, + //obj_id: str, +} + +impl OCToken { + pub fn new() -> Self { + Self { + id: COUNTER.fetch_add(1, Ordering::Relaxed), + } + } +} + +#[derive(Debug, Clone)] +pub struct Marking { + petri_net: Arc, + assignments: HashMap>, +} + +impl Marking { + pub fn new(petri_net: ObjectCentricPetriNet) -> Self { + Marking { + petri_net: petri_net.into(), + assignments: HashMap::new(), + } + } + + pub fn add_initial_token_count(&mut self, place_id: &Uuid, count: u64) { + if !self + .petri_net + .get_place(place_id) + .expect("Place not found") + .initial + { + panic!("Place {} is not an initial place", place_id); + } + + // create a new token count times and add it to a new hashbag + let bag = self + .assignments + .entry(place_id.clone()) + .or_insert_with(|| HashBag::new()); + + for _ in 0..count { + let token = OCToken::new(); + bag.insert(token); + } + } + + pub fn add_initial_tokens(&mut self, place_id: &Uuid, tokens: &HashBag) { + if !self + .petri_net + .get_place(place_id) + .expect("Place not found") + .initial + { + panic!("Place {} is not an initial place", place_id); + } + + self._add_all_tokens_unsafe(place_id, tokens); + } + + /// Adds a token to a place, regardless of the permissibility of the operation. + /// It is strictly recommended to use add_initial_tokens instead + pub fn _add_all_tokens_unsafe(&mut self, place_id: &Uuid, tokens: &HashBag) { + let bag = self + .assignments + .entry(place_id.clone()) + .or_insert_with(|| HashBag::new()); + + tokens.set_iter().for_each(|(token, count)| { + bag.insert_many(token.clone(), count); + }); + } + + pub fn add_token_unsafe(&mut self, place_id: Uuid, token: OCToken) { + self.assignments + .entry(place_id) + .or_insert_with(|| HashBag::new()) + .insert(token); + } + + /// Returns all possible firing combinations for the given transition. + pub fn get_firing_combinations( + &self, + transition: &Transition, + ) -> Vec { + let arcs_to_place: HashMap> = + transition + .input_arcs + .iter() + .fold(HashMap::new(), |mut acc, arc| { + acc.entry(arc.source_place_id) + .or_insert_with(Vec::new) + .push(arc); + acc + }); + + let default: HashBag = HashBag::new(); + let mut input_place_map: HashMap, usize)>> = + HashMap::new(); + + // Group input places by object_type along with their required token counts + for (place_id, arcs) in arcs_to_place.iter() { + let place = self.petri_net.get_place(place_id).expect("Place not found"); + let obj_type = place.object_type.clone(); + let consuming_arc_count = arcs.len(); + let bag = self.assignments.get(place_id).unwrap_or(&default); + + // Filter the bag to retain only tokens with a count >= consuming_arc_count + let mut filtered_bag = bag.clone(); + filtered_bag.retain(|_, count| { + if count >= consuming_arc_count { + return count; + } + return 0; + }); + + input_place_map + .entry(obj_type) + .or_insert_with(Vec::new) + .push((place_id, filtered_bag, consuming_arc_count)); + } + + let mut obj_type_tokens: Vec>> = Vec::new(); + + // For each object type, find tokens that satisfy all input places + for (_obj_type, places) in input_place_map.iter() { + let common_tokens = + intersect_hashbags(&*places.iter().map(|(_, bag, _)| bag).collect::>()); + + // If there are no common tokens, we can't fire the transition + if (common_tokens.len() == 0) { + return vec![]; + } + + // TODO add variable arc support + + obj_type_tokens.push( + common_tokens + .set_iter() + .map(|(token, _)| { + places + .iter() + .map(|(place_id, _, req)| { + (PlaceBindingInfo { + consumed: req.clone(), + token: token.clone(), + place_id: *place_id.clone(), + }) + }) + .collect() + }) + .collect(), + ) + } + + // Compute cartesian product of tokens across all object types + if obj_type_tokens.is_empty() { + return vec![]; + } + + let product = cartesian_product_iter(obj_type_tokens); + + // Convert each product into a firing combination map + + product.into_iter().map(|combination| { + Binding::from_combinations(transition.id, combination) + }).collect() + } + + /// Checks if the transition is enabled by verifying if there is at least one firing combination. + pub fn is_enabled(&self, transition: &Transition) -> bool { + !self.get_firing_combinations(transition).is_empty() + } + + /* pub fn compute_possible_firings(&self) -> Vec { + self.petri_net + .transitions + .iter() + .filter(|t| self.is_enabled(t)) + .map(|t| t.id.clone()) + .collect() + }*/ +} + +impl Binding { + fn from_combinations(transition_id: Uuid, combinations: Vec>) -> Self { + Binding { + tokens: combinations + .iter() + .fold(HashMap::new(), |mut acc, bindings| { + bindings.into_iter().for_each(|binding| { + acc.insert(binding.place_id, binding.clone()); // fixme clone + }); + acc + }), + transition_id, + } + } +} + +#[derive(Debug, Clone)] +struct PlaceBindingInfo { + pub place_id: Uuid, + pub consumed: usize, + pub token: OCToken, +} +struct Binding { + /// Tokens to take out of the place + pub tokens: HashMap, + pub transition_id: Uuid, +} + +fn cartesian_product<'a, T>(inputs: Vec>) -> Vec> { + inputs.into_iter().fold(vec![Vec::new()], |acc, pool| { + acc.into_iter() + .flat_map(|combination| { + pool.iter().map(move |&item| { + let mut new_combination = combination.clone(); + new_combination.push(item); + new_combination + }) + }) + .collect() + }) +} + +fn cartesian_product_iter(inputs: Vec>) -> Vec> { + inputs.into_iter().fold(vec![Vec::new()], |acc, pool| { + acc.into_iter() + .flat_map(|combination| { + pool.iter().map(move |item| { + let mut new_combination = combination.clone(); + new_combination.push(item.clone()); + new_combination + }) + }) + .collect() + }) +} diff --git a/process_mining/src/oc_petri_net/mod.rs b/process_mining/src/oc_petri_net/mod.rs new file mode 100644 index 0000000..a8853de --- /dev/null +++ b/process_mining/src/oc_petri_net/mod.rs @@ -0,0 +1,3 @@ +pub mod oc_petri_net; +mod marking; +mod util; \ No newline at end of file diff --git a/process_mining/src/oc_petri_net/oc_petri_net.rs b/process_mining/src/oc_petri_net/oc_petri_net.rs new file mode 100644 index 0000000..ef6e1a0 --- /dev/null +++ b/process_mining/src/oc_petri_net/oc_petri_net.rs @@ -0,0 +1,480 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; +use uuid::Uuid; + +/// Macro to implement `PartialEq`, `Eq`, and `Hash` based on `id` for structs. +#[macro_export] +macro_rules! id_based_impls { + ($struct_name:ident) => { + impl PartialEq for $struct_name { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } + } + + impl Eq for $struct_name {} + + impl Hash for $struct_name { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } + } + }; +} + +#[derive(Debug, Clone)] +pub struct InputArc { + pub id: Uuid, + pub source_place_id: Uuid, // Place ID + pub target_transition_id: Uuid, // Transition ID + pub variable: bool, + pub weight: i32, +} + +#[derive(Debug, Clone)] +pub struct OutputArc { + pub id: Uuid, + pub source_transition_id: Uuid, // Transition ID + pub target_place_id: Uuid, // Place ID + pub variable: bool, + pub weight: i32, +} + +impl InputArc { + pub fn new( + source_place_id: Uuid, + target_transition_id: Uuid, + variable: bool, + weight: i32, + ) -> Self { + InputArc { + id: Uuid::new_v4(), + source_place_id, + target_transition_id, + variable, + weight, + } + } +} + +impl OutputArc { + pub fn new( + source_transition_id: Uuid, + target_place_id: Uuid, + variable: bool, + weight: i32, + ) -> Self { + OutputArc { + id: Uuid::new_v4(), + source_transition_id, + target_place_id, + variable, + weight, + } + } +} + +id_based_impls!(InputArc); +id_based_impls!(OutputArc); + +#[derive(Debug, Clone)] +pub struct Place { + pub id: Uuid, + pub name: Option, + pub object_type: String, + pub initial: bool, + pub final_place: bool, + pub input_arcs: HashSet>, // Incoming arcs (OutputArcs) + pub output_arcs: HashSet>, // Outgoing arcs (InputArcs) +} + +#[derive(Debug, Clone)] +pub struct Transition { + pub id: Uuid, + pub name: String, + pub label: Option, + pub silent: bool, + pub input_arcs: HashSet>, // Incoming arcs (InputArcs) + pub output_arcs: HashSet>, // Outgoing arcs (OutputArcs) +} + +id_based_impls!(Place); +id_based_impls!(Transition); + +#[derive(Debug, Clone)] +pub struct ObjectCentricPetriNet { + // Stores entities by their IDs for quick access + pub places: HashMap, + pub transitions: HashMap, + // Stores arcs by their IDs for easy access and management + pub input_arcs: HashMap>, + pub output_arcs: HashMap>, +} + +impl ObjectCentricPetriNet { + pub fn new() -> Self { + ObjectCentricPetriNet { + places: HashMap::new(), + transitions: HashMap::new(), + input_arcs: HashMap::new(), + output_arcs: HashMap::new(), + } + } + + // Place Operations + pub fn add_place( + &mut self, + name: Option, + object_type: String, + initial: bool, + final_state: bool, + ) -> Place { + let place = Place { + id: Uuid::new_v4(), + name, + object_type, + initial, + final_place: final_state, + input_arcs: HashSet::new(), + output_arcs: HashSet::new(), + }; + self.places.insert(place.id, place.clone()); + place + } + + pub fn get_place(&self, id: &Uuid) -> Option<&Place> { + self.places.get(id) + } + + pub fn get_initial_places(&self) -> Vec { + self.places + .values() + .filter(|place| place.initial) + .cloned() + .collect() + } + + pub fn get_final_places(&self) -> Vec { + self.places + .values() + .filter(|place| place.final_place) + .cloned() + .collect() + } + + // Transition Operations + pub fn add_transition( + &mut self, + name: String, + label: Option, + silent: bool, + ) -> Transition { + let transition = Transition { + id: Uuid::new_v4(), + name, + label, + silent, + input_arcs: HashSet::new(), + output_arcs: HashSet::new(), + }; + self.transitions.insert(transition.id, transition.clone()); + transition + } + + pub fn get_transition(&self, id: &Uuid) -> Option<&Transition> { + self.transitions.get(id) + } + + // Arc Operations + pub fn add_input_arc( + &mut self, + source_place_id: Uuid, + target_transition_id: Uuid, + variable: bool, + weight: i32, + ) -> Arc { + // Validate existence + let source_place = match self.places.get(&source_place_id) { + Some(place) => place.clone(), + None => panic!("Source place with ID {:?} does not exist.", source_place_id), + }; + let target_transition = match self.transitions.get(&target_transition_id) { + Some(transition) => transition.clone(), + None => panic!( + "Target transition with ID {:?} does not exist.", + target_transition_id + ), + }; + let arc = Arc::new(InputArc::new( + source_place_id, + target_transition_id, + variable, + weight, + )); + self.input_arcs.insert(arc.id, Arc::clone(&arc)); + // Update relationships + let place_arc = Arc::clone(&arc); + self.places.get_mut(&source_place_id).unwrap().output_arcs.insert(place_arc.clone()); + self.transitions + .get_mut(&target_transition_id) + .unwrap() + .input_arcs + .insert(place_arc); + arc + } + + pub fn add_output_arc( + &mut self, + source_transition_id: Uuid, + target_place_id: Uuid, + variable: bool, + weight: i32, + ) -> Arc { + // Validate existence + let source_transition = match self.transitions.get(&source_transition_id) { + Some(transition) => transition.clone(), + None => panic!( + "Source transition with ID {:?} does not exist.", + source_transition_id + ), + }; + let target_place = match self.places.get(&target_place_id) { + Some(place) => place.clone(), + None => panic!( + "Target place with ID {:?} does not exist.", + target_place_id + ), + }; + let arc = Arc::new(OutputArc::new( + source_transition_id, + target_place_id, + variable, + weight, + )); + self.output_arcs.insert(arc.id, Arc::clone(&arc)); + // Update relationships + let transition_arc = Arc::clone(&arc); + self.transitions + .get_mut(&source_transition_id) + .unwrap() + .output_arcs + .insert(transition_arc.clone()); + self.places + .get_mut(&target_place_id) + .unwrap() + .input_arcs + .insert(transition_arc); + arc + } + + pub fn get_input_arc(&self, id: &Uuid) -> Option> { + self.input_arcs.get(id).cloned() + } + + pub fn get_output_arc(&self, id: &Uuid) -> Option> { + self.output_arcs.get(id).cloned() + } + + // Pre and Post Set Methods for Places + pub fn get_pre_set_of_place(&self, place_id: &Uuid) -> Vec { + self.places.get(place_id).map_or(Vec::new(), |place| { + place + .input_arcs + .iter() + .filter_map(|arc| self.transitions.get(&arc.source_transition_id).cloned()) + .collect() + }) + } + + pub fn get_post_set_of_place(&self, place_id: &Uuid) -> Vec { + self.places.get(place_id).map_or(Vec::new(), |place| { + place + .output_arcs + .iter() + .filter_map(|arc| self.transitions.get(&arc.target_transition_id).cloned()) + .collect() + }) + } + + pub fn get_pre_set_of_transition(&self, transition_id: &Uuid) -> Vec { + self.transitions.get(transition_id).map_or(Vec::new(), |transition| { + transition + .input_arcs + .iter() + .filter_map(|arc| self.places.get(&arc.source_place_id).cloned()) + .collect() + }) + } + + pub fn get_post_set_of_transition(&self, transition_id: &Uuid) -> Vec { + self.transitions.get(transition_id).map_or(Vec::new(), |transition| { + transition + .output_arcs + .iter() + .filter_map(|arc| self.places.get(&arc.target_place_id).cloned()) + .collect() + }) + } + + // Additional Helper Methods (Optional) + /// Retrieves all input arcs for a given place + pub fn get_input_arcs_for_place(&self, place_id: &Uuid) -> HashSet> { + self.places + .get(place_id) + .expect("Place not found") + .input_arcs + .clone() + } + + /// Retrieves all output arcs for a given place + pub fn get_output_arcs_for_place(&self, place_id: &Uuid) -> HashSet> { + self.places + .get(place_id) + .expect("Place not found") + .output_arcs + .clone() + } + + /// Retrieves all input arcs for a given transition + pub fn get_input_arcs_for_transition(&self, transition_id: &Uuid) -> HashSet> { + self.transitions + .get(transition_id) + .expect("Transition not found") + .input_arcs + .clone() + } + + /// Retrieves all output arcs for a given transition + pub fn get_output_arcs_for_transition(&self, transition_id: &Uuid) -> HashSet> { + self.transitions + .get(transition_id) + .expect("Transition not found") + .output_arcs + .clone() + } +} + +// Implement functionality for Place and Transition +impl Place { + pub fn new( + name: Option, + object_type: String, + initial: bool, + final_state: bool, + ) -> Self { + Place { + id: Uuid::new_v4(), + name, + object_type, + initial, + final_place: final_state, + input_arcs: HashSet::new(), + output_arcs: HashSet::new(), + } + } +} + +impl Transition { + pub fn new(name: String, label: Option, silent: bool) -> Self { + Transition { + id: Uuid::new_v4(), + name, + label, + silent, + input_arcs: HashSet::new(), + output_arcs: HashSet::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_petri_net() { + let mut net = ObjectCentricPetriNet::new(); + + // Add places + let p1 = net.add_place(Some("P1".to_string()), "Token".to_string(), true, false); + let p2 = net.add_place(Some("P2".to_string()), "Token".to_string(), false, true); + + // Add transitions + let t1 = net.add_transition("T1".to_string(), Some("Transition 1".to_string()), false); + let t2 = net.add_transition("T2".to_string(), None, true); + + // Add arcs + let arc1 = net.add_input_arc(p1.id, t1.id, false, 1); + let arc2 = net.add_output_arc(t1.id, p2.id, false, 1); + let arc3 = net.add_input_arc(p2.id, t2.id, true, 2); + let arc4 = net.add_output_arc(t2.id, p1.id, true, 2); + + // Retrieve pre and post sets for Transition T1 + let pre_t1 = net.get_pre_set_of_transition(&t1.id); + let post_t1 = net.get_post_set_of_transition(&t1.id); + assert_eq!(pre_t1.len(), 1); + assert_eq!(pre_t1[0].id, p1.id); + assert_eq!(post_t1.len(), 1); + assert_eq!(post_t1[0].id, p2.id); + + // Retrieve pre and post sets for Place P1 + let pre_p1 = net.get_pre_set_of_place(&p1.id); + let post_p1 = net.get_post_set_of_place(&p1.id); + assert_eq!(pre_p1.len(), 1); + assert_eq!(pre_p1[0].id, t2.id); + assert_eq!(post_p1.len(), 1); + assert_eq!(post_p1[0].id, t1.id); + + // Additional Assertions + // Verify arc details + let retrieved_arc1 = net.get_input_arc(&arc1.id).unwrap(); + assert_eq!(retrieved_arc1.source_place_id, p1.id); + assert_eq!(retrieved_arc1.target_transition_id, t1.id); + assert!(!retrieved_arc1.variable); + assert_eq!(retrieved_arc1.weight, 1); + + let retrieved_arc4 = net.get_output_arc(&arc4.id).unwrap(); + assert_eq!(retrieved_arc4.source_transition_id, t2.id); + assert_eq!(retrieved_arc4.target_place_id, p1.id); + assert!(retrieved_arc4.variable); + assert_eq!(retrieved_arc4.weight, 2); + } + + #[test] + #[should_panic(expected = "Source place with ID")] + fn test_add_input_arc_invalid_place() { + let mut net = ObjectCentricPetriNet::new(); + let p_id = Uuid::new_v4(); // Non-existent place + let t_id = net.add_transition("T1".to_string(), None, false).id; + net.add_input_arc(p_id, t_id, false, 1); + } + + #[test] + #[should_panic(expected = "Target transition with ID")] + fn test_add_input_arc_invalid_transition() { + let mut net = ObjectCentricPetriNet::new(); + let t_id = Uuid::new_v4(); // Non-existent transition + let p_id = net.add_place(None, "Token".to_string(), false, false).id; + net.add_input_arc(p_id, t_id, false, 1); + } + + #[test] + #[should_panic(expected = "Source transition with ID")] + fn test_add_output_arc_invalid_transition() { + let mut net = ObjectCentricPetriNet::new(); + let t_id = Uuid::new_v4(); // Non-existent transition + let p_id = net.add_place(None, "Token".to_string(), false, false).id; + net.add_output_arc(t_id, p_id, false, 1); + } + + #[test] + #[should_panic(expected = "Target place with ID")] + fn test_add_output_arc_invalid_place() { + let mut net = ObjectCentricPetriNet::new(); + let t_id = net.add_transition("T1".to_string(), None, false).id; + let p_id = Uuid::new_v4(); // Non-existent place + net.add_output_arc(t_id, p_id, false, 1); + } +} \ No newline at end of file diff --git a/process_mining/src/oc_petri_net/util/intersect_hashbag.rs b/process_mining/src/oc_petri_net/util/intersect_hashbag.rs new file mode 100644 index 0000000..978659f --- /dev/null +++ b/process_mining/src/oc_petri_net/util/intersect_hashbag.rs @@ -0,0 +1,274 @@ +use hashbag::HashBag; +use std::hash::{BuildHasher, Hash}; +use std::cmp::min; + +/// Intersects multiple `HashBag` instances, returning a new `HashBag` containing only the elements +/// present in **all** input `HashBag`s. The count for each element in the resulting `HashBag` +/// is the minimum count found across all input bags. +/// +/// # Type Parameters +/// +/// - `T`: The type of elements in the `HashBag`. Must implement `Clone`, `Eq`, and `Hash`. +/// - `S`: The hasher used by the first `HashBag`. +/// - `OtherS`: The hasher used by the other `HashBag`s. +/// +/// # Arguments +/// +/// - `bags`: A slice of `HashBag` references to intersect. +/// +/// # Returns +/// +/// A new `HashBag` representing the intersection of all input `HashBag`s. +/// +/// # Panics +/// +/// - If the `bags` slice is empty, it returns an empty `HashBag`. +/// +/// # Examples +/// +/// ```rust +/// use hashbag::HashBag; +/// +/// fn main() { +/// let mut a = HashBag::new(); +/// a.insert("apple"); +/// a.insert("banana"); +/// a.insert("banana"); +/// a.insert("cherry"); +/// +/// let mut b = HashBag::new(); +/// b.insert("banana"); +/// b.insert("banana"); +/// b.insert("dragonfruit"); +/// +/// let mut c = HashBag::new(); +/// c.insert("banana"); +/// c.insert("banana"); +/// c.insert("banana"); // Extra banana +/// +/// let intersection = intersect_hashbags(&[&a, &b, &c]); +/// +/// // The intersection should contain "banana" with a count of 2 +/// assert_eq!(intersection.len(), 2); +/// assert_eq!(intersection.contains("banana"), 2); +/// assert_eq!(intersection.contains("apple"), 0); +/// assert_eq!(intersection.contains("cherry"), 0); +/// assert_eq!(intersection.contains("dragonfruit"), 0); +/// +/// println!("Intersection contains:"); +/// for (item, count) in intersection.set_iter() { +/// println!("{}: {}", item, count); +/// } +/// } +/// ``` +pub fn intersect_hashbags( + bags: &[&HashBag], +) -> HashBag +where + T: Clone + Eq + Hash, + S: BuildHasher + Clone +{ + // Find the bag with the smallest set_len to minimize iterations + let smallest_bag = bags + .iter() + .min_by_key(|bag| bag.set_len()) + .expect("At least one bag must be provided"); + + // Initialize the intersection HashBag with the same hasher as the smallest bag + let mut intersection = HashBag::with_hasher(smallest_bag.hasher().clone()); + + // Iterate over the distinct elements of the smallest bag + for (item, count) in smallest_bag.set_iter() { + // Initialize min_count with the count from the smallest bag + let mut min_count = count; + + // Check the count of the current item in all other bags + for bag in bags.iter() { + if *bag == *smallest_bag { + continue; // Skip the smallest bag itself + } + + let other_count = bag.contains(item); + + if other_count == 0 { + min_count = 0; + break; // Item not present in one of the bags; no need to check further + } + + // Update min_count to be the minimum so far + min_count = min(min_count, other_count); + + // Early termination if min_count reaches zero + if min_count == 0 { + break; + } + } + + // If the item is present in all bags, insert it with min_count + if min_count > 0 { + intersection.insert_many(item.clone(), min_count); + } + } + + intersection +} + +#[cfg(test)] +mod tests { + use super::*; + use hashbag::HashBag; + + /// Test intersecting two HashBags with overlapping elements. + #[test] + fn test_basic_intersection() { + let mut a = HashBag::new(); + a.insert("apple"); + a.insert("banana"); + a.insert("banana"); + a.insert("cherry"); + + let mut b = HashBag::new(); + b.insert("banana"); + b.insert("banana"); + b.insert("dragonfruit"); + + let intersection = intersect_hashbags(&[&a, &b]); + + // "banana" appears twice in both bags + assert_eq!(intersection.len(), 2); + assert_eq!(intersection.contains("banana"), 2); + + // Elements not common to both should not appear + assert_eq!(intersection.contains("apple"), 0); + assert_eq!(intersection.contains("cherry"), 0); + assert_eq!(intersection.contains("dragonfruit"), 0); + } + + /// Test intersecting three HashBags with overlapping and non-overlapping elements. + #[test] + fn test_multiple_intersection() { + let mut a = HashBag::new(); + a.insert("apple"); + a.insert("banana"); + a.insert("banana"); + a.insert("cherry"); + + let mut b = HashBag::new(); + b.insert("banana"); + b.insert("banana"); + b.insert("dragonfruit"); + + let mut c = HashBag::new(); + c.insert("banana"); + c.insert("banana"); + c.insert("banana"); // Extra banana + + let intersection = intersect_hashbags(&[&a, &b, &c]); + + // "banana" appears twice in both `a` and `b`, and three times in `c` + // Minimum count is 2 + assert_eq!(intersection.len(), 2); // 2 counts of "banana" + assert_eq!(intersection.contains("banana"), 2); + + // Elements not common to all should not appear + assert_eq!(intersection.contains("apple"), 0); + assert_eq!(intersection.contains("cherry"), 0); + assert_eq!(intersection.contains("dragonfruit"), 0); + } + + /// Test intersecting HashBags with no common elements results in an empty HashBag. + #[test] + fn test_no_common_elements() { + let mut a = HashBag::new(); + a.insert("apple"); + a.insert("banana"); + + let mut b = HashBag::new(); + b.insert("cherry"); + b.insert("dragonfruit"); + + let intersection = intersect_hashbags(&[&a, &b]); + + // No common elements, intersection should be empty + assert!(intersection.is_empty()); + } + + /// Test intersecting identical HashBags results in the same HashBag. + #[test] + fn test_identical_bags() { + let mut a = HashBag::new(); + a.insert("apple"); + a.insert("banana"); + a.insert("banana"); + a.insert("cherry"); + + let mut b = HashBag::new(); + b.insert("apple"); + b.insert("banana"); + b.insert("banana"); + b.insert("cherry"); + + let intersection = intersect_hashbags(&[&a, &b]); + + // The intersection should be identical to the original bags + assert_eq!(intersection.len(), a.len()); + assert_eq!(intersection, a); + assert_eq!(intersection, b); + } + + /// Test that intersect_hashbags panics when provided with an empty list. + #[test] + #[should_panic(expected = "At least one bag must be provided")] + fn test_empty_list_panic() { + // Attempting to intersect an empty list should panic + let intersection: HashBag<&str> = intersect_hashbags(&[]); + } + + /// Test intersecting multiple HashBags where some have varying counts of a common element. + #[test] + fn test_varying_counts() { + let mut a = HashBag::new(); + a.insert("kiwi"); + a.insert("kiwi"); + a.insert("kiwi"); + a.insert("melon"); + a.insert("mango"); + a.insert("mango"); + + let mut b = HashBag::new(); + b.insert("kiwi"); + b.insert("kiwi"); + b.insert("melon"); + + let mut c = HashBag::new(); + c.insert("kiwi"); + c.insert("melon"); + c.insert("melon"); + + let intersection = intersect_hashbags(&[&a, &b, &c]); + + // "kiwi" appears 3 times in `a`, 2 times in `b`, and 1 time in `c` + // Minimum count is 1 + assert_eq!(intersection.len(), 2); // 1 count of "kiwi" 1 count of "melon" + assert_eq!(intersection.contains("kiwi"), 1); + assert_eq!(intersection.contains("melon"), 1); + + // "mango" and "melon" are not common to all + assert_eq!(intersection.contains("mango"), 0); + } + + /// Test intersecting with a single HashBag returns a copy of that HashBag. + #[test] + fn test_single_bag_intersection() { + let mut a = HashBag::new(); + a.insert("apple"); + a.insert("banana"); + a.insert("banana"); + + let intersection = intersect_hashbags(&[&a]); + + // Intersection with a single bag should be identical to that bag + assert_eq!(intersection.len(), a.len()); + assert_eq!(intersection, a); + } +} \ No newline at end of file diff --git a/process_mining/src/oc_petri_net/util/mod.rs b/process_mining/src/oc_petri_net/util/mod.rs new file mode 100644 index 0000000..f5b4e1d --- /dev/null +++ b/process_mining/src/oc_petri_net/util/mod.rs @@ -0,0 +1 @@ +pub mod intersect_hashbag; \ No newline at end of file