diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 79a4fea3..1ff7cbde 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,24 +4,9 @@ ## Content -- Scope: - - [ ] Code: if relevant, add affected target - - [ ] Build script: if relevant, add affected target - - [ ] Documentation - - [ ] CI - - [ ] Repository - - [ ] Other: ... -- Type of change: - - [ ] New feature(s) - - [ ] Fix: if relevant, add issue #... - - [ ] Testing - - [ ] Refactor - - [ ] Revert - - [ ] Chore -- Other: - - [ ] Breaking change - - [ ] New dependency -- Necessary follow-up: - - [ ] Needs documentation - - [ ] Needs testing - - [ ] Other: ... +### Main changes + + +## Issues + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b09aad2..51e3cdc3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -187,8 +187,6 @@ jobs: strategy: matrix: platform: - - runner: macos-13 - target: x86_64 - runner: macos-14 target: aarch64 steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d14e23a..cfb19055 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,18 @@ +fail_fast: false repos: -- repo: https://github.com/doublify/pre-commit-rust + - repo: https://github.com/doublify/pre-commit-rust rev: v1.0 + hooks: - # - id: cargo-check - # args: ["--workspace"] - - id: fmt + - id: cargo-check + args: ["--workspace"] + - id: fmt args: ["--", "--check"] - # - id: clippy - # args: ["--", "-D", "warnings"] \ No newline at end of file + - id: clippy + args: ["--", "-D", "warnings"] + + - repo: https://github.com/EmbarkStudios/cargo-deny + rev: 0.19.0 + hooks: + - id: cargo-deny + args: ["--all-features", "check", "ban", "licenses", "sources"] diff --git a/CHANGELOG.md b/CHANGELOG.md index cf1f0752..4f77ae97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # CHANGELOG +## 0.1.4 +*Date: * + +#### Features: + +- Assemble flowmaps + + +#### General Enhancements + +- Improve transitioner api and performance +- Improve documentation and examples +- Improve python bindings + +#### Build System Changes + +- Update and clean dependencies + +#### Breaking Changes + +#### Deprecations +- C Case Reader + +#### Bug Fixes + +#### Known Issue + +- CFD generated model does not compute flows correctly + + + ## 0.1.3 *Date: 11/23/2025* diff --git a/Cargo.lock b/Cargo.lock index 505ac799..b81d8a94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,36 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" -version = "0.6.20" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -25,39 +44,45 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "approx" version = "0.5.1" @@ -79,11 +104,32 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -93,9 +139,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.37" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -103,15 +149,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.47" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -119,9 +165,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -131,9 +177,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -143,23 +189,40 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmtool" -version = "0.1.3" +version = "0.1.4" dependencies = [ "clap", + "cmtool-assemble", "cmtool-core", "cmtool-data", + "nalgebra", + "nalgebra-sparse", "thiserror", ] +[[package]] +name = "cmtool-assemble" +version = "0.1.4" +dependencies = [ + "cmtool-data", + "ndarray", + "serde 1.0.228", + "serde-xml-rs", + "serde_json", + "serde_xml", + "thiserror", + "xsd-parser", +] + [[package]] name = "cmtool-core" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cmtool-data", "enum_dispatch", @@ -169,7 +232,7 @@ dependencies = [ [[package]] name = "cmtool-cxx" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cmtool-data", "cxx", @@ -180,22 +243,22 @@ dependencies = [ [[package]] name = "cmtool-data" -version = "0.1.3" +version = "0.1.4" dependencies = [ "enum_dispatch", "nalgebra", "nalgebra-sparse", "ndarray", - "serde", - "serde_cbor", + "serde 1.0.228", "serde_json", "thiserror", ] [[package]] name = "cmtool-example" -version = "0.1.3" +version = "0.1.4" dependencies = [ + "cmtool-assemble", "cmtool-data", "nalgebra", "nalgebra-sparse", @@ -204,7 +267,7 @@ dependencies = [ [[package]] name = "cmtool-python" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cmtool-data", "nalgebra", @@ -218,16 +281,16 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ - "serde", + "serde 1.0.228", "termcolor", "unicode-width", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "crc32fast" @@ -240,9 +303,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.188" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ac4eaf7ebe29e92f1b091ceefec7710a53a6f6154b2460afda626c113b65b9" +checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" dependencies = [ "cc", "cxx-build", @@ -255,9 +318,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.188" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2abd4c3021eefbac5149f994c117b426852bca3a0aad227698527bca6d4ea657" +checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" dependencies = [ "cc", "codespan-reporting", @@ -270,9 +333,9 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.188" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f12fbc5888b2311f23e52a601e11ad7790d8f0dbb903ec26e2513bf5373ed70" +checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" dependencies = [ "clap", "codespan-reporting", @@ -284,15 +347,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.188" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d3dd7870af06e283f3f8ce0418019c96171c9ce122cfb9c8879de3d84388fd" +checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" [[package]] name = "cxxbridge-macro" -version = "1.0.188" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26f0d82da663316786791c3d0e9f9edc7d1ee1f04bdad3d2643086a69d6256c" +checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" dependencies = [ "indexmap", "proc-macro2", @@ -300,6 +363,26 @@ dependencies = [ "syn", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -320,15 +403,15 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "find-msvc-tools" -version = "0.1.1" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -340,6 +423,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "glam" version = "0.14.0" @@ -432,21 +524,15 @@ checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" [[package]] name = "glam" -version = "0.30.6" +version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8e8d9db3feacb0bb4801b77e00c4d550f3e8d3e5303388fdfec3ff8a91f04d" - -[[package]] -name = "half" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -454,39 +540,141 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", ] [[package]] -name = "indoc" -version = "2.0.6" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "itoa" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] -name = "itoa" -version = "1.0.15" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "link-cplusplus" @@ -497,17 +685,41 @@ dependencies = [ "cc", ] +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" -version = "0.4.28" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.29", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lz4_flex" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" dependencies = [ "twox-hash", ] @@ -535,18 +747,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" @@ -555,13 +758,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "nalgebra" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd59afb6639828b33677758314a4a1a745c15c02bc597095b851c8fd915cf49" +checksum = "c4d5b3eff5cd580f93da45e64715e8c20a3996342f1e466599cf7a267a0c2f5f" dependencies = [ "approx", "glam 0.14.0", @@ -579,7 +783,7 @@ dependencies = [ "glam 0.27.0", "glam 0.28.0", "glam 0.29.3", - "glam 0.30.6", + "glam 0.30.10", "matrixmultiply", "nalgebra-macros", "num-complex", @@ -612,9 +816,9 @@ dependencies = [ [[package]] name = "ndarray" -version = "0.16.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" dependencies = [ "matrixmultiply", "num-complex", @@ -695,9 +899,9 @@ dependencies = [ [[package]] name = "numpy" -version = "0.24.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cfbf3f0feededcaa4d289fe3079b03659e85c5b5a177f4ba6fb01ab4fb3e39" +checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2" dependencies = [ "libc", "ndarray", @@ -711,15 +915,38 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] [[package]] name = "paste" @@ -727,6 +954,18 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkg-config" version = "0.3.32" @@ -735,61 +974,65 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.24.2" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" dependencies = [ "libc", "pyo3-build-config", @@ -797,9 +1040,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -809,9 +1052,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" dependencies = [ "heck", "proc-macro2", @@ -827,14 +1070,24 @@ source = "git+https://github.com/elrnv/quick-xml.git?branch=binary-support4#d66f dependencies = [ "memchr", "ref-cast", - "serde", + "serde 1.0.228", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "encoding_rs", + "memchr", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -845,20 +1098,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -866,16 +1128,39 @@ dependencies = [ ] [[package]] -name = "rustc-hash" -version = "2.1.1" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "ryu" -version = "1.0.20" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "safe_arch" @@ -886,6 +1171,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "scratch" version = "1.0.9" @@ -894,38 +1185,46 @@ checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" [[package]] name = "serde" -version = "1.0.225" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] -name = "serde_cbor" -version = "0.11.2" +name = "serde-xml-rs" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ - "half", - "serde", + "log 0.4.29", + "serde 1.0.228", + "thiserror", + "xml", ] [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -934,15 +1233,25 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", - "serde", + "serde 1.0.228", "serde_core", + "zmij", +] + +[[package]] +name = "serde_xml" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56346e526b0828da6b7a6a867076a6ae22d188ffd7a5511b4ffbd815def0ba95" +dependencies = [ + "log 0.3.9", + "serde 0.8.23", ] [[package]] @@ -964,6 +1273,24 @@ dependencies = [ "wide", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -972,20 +1299,31 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "target-lexicon" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "termcolor" @@ -998,24 +1336,65 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "trim-in-place" version = "0.1.7" @@ -1030,15 +1409,15 @@ checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -1052,6 +1431,24 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde 1.0.228", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1061,19 +1458,19 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vtkio" version = "0.7.0-rc1" -source = "git+https://github.com/elrnv/vtkio.git#e14c8ff3caf84ae5e21c2ee70aa18275d6e54051" +source = "git+https://github.com/elrnv/vtkio.git#942ce7e27f172cb3c5a04ec6179dd7f0417d1e82" dependencies = [ "base64", "bytemuck", "byteorder", "flate2", - "log", + "log 0.4.29", "lz4_flex", "nom", "num-derive", "num-traits", - "quick-xml", - "serde", + "quick-xml 0.36.0", + "serde 1.0.228", "trim-in-place", "xz2", ] @@ -1099,89 +1496,157 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] -name = "windows-targets" -version = "0.53.3" +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xml" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" + +[[package]] +name = "xsd-parser" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "cf636856509b72e6ce6d84232fdf0a6fb48ce9a04767bf6dc4f7292fa852940e" dependencies = [ - "windows-link", - "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", + "Inflector", + "anyhow", + "base64", + "bit-set", + "bitflags", + "indexmap", + "parking_lot", + "proc-macro2", + "quick-xml 0.38.4", + "quote", + "regex", + "smallvec", + "thiserror", + "tracing", + "unindent", + "url", + "xsd-parser-types", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" +name = "xsd-parser-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "5a6c01d5f57551a048047e9f28ed5952e3ceab84882832e4b11a748c09122a4a" +dependencies = [ + "encoding_rs", + "indexmap", + "quick-xml 0.38.4", + "regex", + "thiserror", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" +name = "xz2" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] [[package]] -name = "windows_i686_gnu" -version = "0.53.0" +name = "yoke" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" +name = "yoke-derive" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_i686_msvc" -version = "0.53.0" +name = "zerofrom" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" +name = "zerotrie" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" +name = "zerovec" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] [[package]] -name = "xz2" -version = "0.1.7" +name = "zerovec-derive" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ - "lzma-sys", + "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 bda936f0..54397c06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,18 @@ [workspace] -members = ["cmtool-data", "cmtool-core", "cmtool", "cmtool-python","cmtool-cxx" ,"example"] +members = [ + "cmtool-data", + "cmtool-core", + "cmtool", + "cmtool-python", + "cmtool-cxx", + 'cmtool-assemble', + "examples", +] resolver = "3" metadata.crane.name = "cmtool" [workspace.package] -version = "0.1.3" +version = "0.1.4" edition = "2024" authors = ["Benjamin Casale"] description = "Compartment Modelling Approach tool" @@ -27,17 +35,22 @@ overflow-checks = false [workspace.dependencies] cmtool-core = { path = "./cmtool-core" } cmtool-data = { path = "./cmtool-data" } +cmtool-assemble = { path = "./cmtool-assemble" } + serde = { version = "1.0.219", features = ["derive"] } -serde_cbor = "0.11.2" +#serde_cbor = "0.11.2" serde_json = "1.0.140" +serde-xml-rs = "0.8.1" +serde_xml = "0.9.1" thiserror = "2.0.12" enum_dispatch = "0.3.13" clap = { version = "4.5.41", features = ["derive"] } - +xsd-parser = "1.5.0" # -nalgebra = "0.34.0" +nalgebra = "0.34.1" nalgebra-sparse = "0.11.0" -ndarray = "0.16.1" +ndarray = "0.17.2" # python -pyo3 = { version = "0.24.0", features = ["abi3-py310", "extension-module"] } +numpy = "0.28.0" +pyo3 = { version = "0.28.1", features = ["abi3-py310", "extension-module"] } diff --git a/README.md b/README.md index 7a640994..003c4c69 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,43 @@ # RCMTool The specific goal of this repository is to develop a Compartment Modelling Tool. -A reactor compartment model is composed of two main parts: -- Flows between compartments (referred to as flowmaps) -- Scalar fields, with values within each compartment +Compartmental Modeling Approach (CMA) is a method that simplifies spatial dimensions by dividing the domain into smaller, uniform "compartments." Each compartment behaves as a homogeneous unit, connected to its neighboring compartments. The domain is represented by a scalar field, and the interactions between compartments are governed by a flowmap. -The compartmentalization process aims to reduce computational costs while maintaining a balance between computational efficiency and accuracy. +This approach significantly reduces computational complexity while maintaining an acceptable level of model accuracy. -The core feature of the crate focuses on processing and simplifying Computational Fluid Dynamics (CFD) results obtained from simulations on fine computational grids into coarser grids, referred to as compartments. The provided tools enable convenient manipulation of compartment data and the configuration of simulations. -Additional features include the capability to generate Compartment Models directly from predefined reactor models (e.g., Plug Flow RReactor Model) +### Objectives -## Objectives +This crate is a port of an existing C++ tool. The objective of the Rust implementation is to evaluate the efficiency of the Rust language to perform this type of operation, with a particular focus on maintaining code readability, maintainability, and performance. -This crate is a port of an existing C++ tool. The objective of the Rust implementation is to evaluate the efficiency of the Rust language for performing this type of operation, with a particular focus on maintaining code readability, maintainability, and performance. +## Overview -### References +This crates aims to provides different feature to perform complete simulation using CMA. -- Ensight gold specifications : - - https://dav.lbl.gov/archive/NERSC/Software/ensight/doc/Manuals/UserManual.pdf +- Unified data type that can easily be written and read +- A Modeler to generate predefined simple model +- A CFD-to-CMA generator to use CFD results exported in Ensight-Gold Format +- Assemble flowmaps seamlessly + +## Crate organization + - cmtool: main cli + - cmtool-data: utilities for high and low level manipulation on flowmaps,compartment data + - cmtool-core: algorithm to transform results from fine-mesh transient CFD simuation into Compartment Models. + - cmtool-assemble: Generate flowmaps from from models and assemble them + - cmtool-python: Python bindings for cmtool-core,FlowmapTransitioner and case reading + - cmtool-cxx: Bindings for C++ use, expose FlowMapTranstioner API + + +## Getting started + +```sh +cargo run --example [NAME] +``` ## Authors - **CASALE Benjamin** ## LICENCE -This work is under GNU Lesser General Public License v3.0 or later (LGPL-3.0-or-later) \ No newline at end of file +This work is under GNU Lesser General Public License v3.0 or later (LGPL-3.0-or-later) diff --git a/cmtool-assemble/Cargo.toml b/cmtool-assemble/Cargo.toml new file mode 100644 index 00000000..eb918a53 --- /dev/null +++ b/cmtool-assemble/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cmtool-assemble" +edition.workspace = true +version.workspace = true +authors.workspace = true +description.workspace = true +documentation.workspace = true +license.workspace = true +[lib] +name = "cmtool_assemble" +path = "src/lib.rs" + +[build-dependencies] +xsd-parser.workspace=true + +[dependencies] +ndarray.workspace = true +cmtool-data.workspace = true +serde_xml.workspace = true +serde-xml-rs.workspace = true +serde.workspace = true +thiserror.workspace = true +serde_json.workspace = true diff --git a/cmtool-assemble/build.rs b/cmtool-assemble/build.rs new file mode 100644 index 00000000..774b4f3e --- /dev/null +++ b/cmtool-assemble/build.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::fs::File; +use std::fs; +use std::io::Write; +use xsd_parser::{ + Config, Error, + config::{GeneratorFlags, InterpreterFlags, OptimizerFlags, ParserFlags, RenderStep, Schema}, + generate, +}; + +static ROOT: &str = "./datamodel"; + +fn domain_schema() -> Result<(), Box> { + // let files = [ + // format!("{}/units.xsd", ROOT), + // format!("{}/reactors.xsd", ROOT), + // format!("{}/connections.xsd", ROOT), + // format!("{}/main.xsd", ROOT), + // ]; + let mut cfg = Config::default().with_schema(Schema::File(format!("{}/main.xsd", ROOT).into())); + cfg = cfg.set_parser_flags(ParserFlags::RESOLVE_INCLUDES | ParserFlags::DEFAULT_NAMESPACES); + cfg = cfg.with_render_steps([ + //RenderStep::Types, + RenderStep::Defaults, + RenderStep::TypesSerdeXmlRs { + version: xsd_parser::config::SerdeXmlRsVersion::Version08AndAbove, + }, + // RenderStep::NamespaceConstants, + + // RenderStep::QuickXmlDeserialize { + // boxed_deserializer: false, + // }, + // RenderStep::TypesSerdeQuickXml, + ]); + cfg = cfg.with_derive(["Debug", "Clone"]); + + cfg.interpreter.flags = InterpreterFlags::all() + - InterpreterFlags::WITH_NUM_BIG_INT + - InterpreterFlags::WITH_XS_ANY_TYPE; + + cfg.optimizer.flags = OptimizerFlags::all(); + + cfg.generator.flags = GeneratorFlags::all() + - GeneratorFlags::USE_MODULES + - GeneratorFlags::USE_NAMESPACE_MODULES + - GeneratorFlags::USE_SCHEMA_MODULES; + + let code = generate(cfg).expect("Failed to generate Rust code from XSD"); + + let mut file = File::create("src/parser/generated_domain.rs").unwrap(); + + file.write_all( + b" +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(unused_imports)] +", + ) + .unwrap(); + + file.write_all(code.to_string().as_bytes()).unwrap(); + Ok(()) +} + +fn main() -> Result<(), Box> { + domain_schema()?; + + println!("cargo:rerun-if-changed=cmtool-assemble/build.rs"); + Ok(()) +} diff --git a/cmtool-assemble/datamodel/connections.xsd b/cmtool-assemble/datamodel/connections.xsd new file mode 100644 index 00000000..26062269 --- /dev/null +++ b/cmtool-assemble/datamodel/connections.xsd @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmtool-assemble/datamodel/main.xsd b/cmtool-assemble/datamodel/main.xsd new file mode 100644 index 00000000..08e9951d --- /dev/null +++ b/cmtool-assemble/datamodel/main.xsd @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/cmtool-assemble/datamodel/reactors.xsd b/cmtool-assemble/datamodel/reactors.xsd new file mode 100644 index 00000000..16f0ab97 --- /dev/null +++ b/cmtool-assemble/datamodel/reactors.xsd @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmtool-assemble/datamodel/units.xsd b/cmtool-assemble/datamodel/units.xsd new file mode 100644 index 00000000..7010e4e0 --- /dev/null +++ b/cmtool-assemble/datamodel/units.xsd @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmtool-assemble/src/data.rs b/cmtool-assemble/src/data.rs new file mode 100644 index 00000000..0ed27333 --- /dev/null +++ b/cmtool-assemble/src/data.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, PartialEq)] +pub enum FlowDirection { + In, + Out, +} + +///Required information to describe a feed +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct FeedFlow { + pub flow: f64, + pub position: usize, + pub output_position: Option, +} + +///Set of feed for each phase and their id +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct ParsedFeeds { + pub liq: HashMap, + pub gas: HashMap, +} + +///Basic about domain +//TODO: clean what's should be private or not +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct DomainInfo { + ///Compartment index offset for given reactor (e.g compartment_cumsum[reactor_id]==10) + pub compartment_cumsum: HashMap, + pub total_number_compartment: usize, + ///Reactor id of pfr names, needed to ensure global mass balance + pub pfr_names: Vec, + //TODO improve it + ///Indicates if case only contains cfd-based reator + pub cm_case_only: Option, + pub is_two_phase_flow: bool, +} +impl DomainInfo { + pub fn get_relative_compartment_number( + &self, + reactor_id: &str, + relative_index: usize, + ) -> Option { + self.compartment_cumsum + .get(reactor_id) + .map(|cum_sum| relative_index + *cum_sum) + } +} + +///Details about generated domain +//TODO: clean what's should be private or not +#[derive(Clone, Serialize, Deserialize, Default)] +pub struct DomainData { + ///connections between partial flowmaps + pub connections: Option<[cmtool_data::RawDataFlux; 2]>, + pub info: DomainInfo, + ///Information about feed of the resulting merged domain + pub feeds: Option, + pub case_path: String, + pub run_id: String, +} diff --git a/cmtool/src/generators.rs b/cmtool-assemble/src/generators.rs similarity index 74% rename from cmtool/src/generators.rs rename to cmtool-assemble/src/generators.rs index d0abfab9..01cd9bef 100644 --- a/cmtool/src/generators.rs +++ b/cmtool-assemble/src/generators.rs @@ -1,11 +1,11 @@ -use cmtool_data::CMCase; -use cmtool_data::CMCaseReader; -use cmtool_data::CMCaseWriter; -use cmtool_data::CMExportType; -use cmtool_data::PhaseCM; -use cmtool_data::RawData; -use cmtool_data::RawPhase; -use cmtool_data::{CMAExportType, CMCaseJson, RawDataFlux, RawDataScalar, RawFlux}; +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::CMError; + +use cmtool_data::{ + CMAExportType, CMCase, CMCaseJson, CMCaseReader, CMCaseWriter, CMExportType, PhaseCM, RawData, + RawDataFlux, RawDataScalar, RawFlux, RawPhase, +}; use std::path::Path; const LIQUID_PAIR: (CMAExportType, CMAExportType) = @@ -17,34 +17,64 @@ type PairType = (CMAExportType, CMAExportType); const PAIRS: (PairType, PairType) = (LIQUID_PAIR, GAS_PAIR); -use thiserror::Error; -#[derive(Error, Debug)] -pub enum CmtoolError { - #[error("Cmtool: {0}")] - Data(#[from] cmtool_data::DataError), - - #[error("Cmtool: {0}")] - Core(#[from] cmtool_core::CoreError), - - #[error("Cmtool: {0}")] - Custom(String), +pub struct PFRDescription { + pub n_compartment: usize, + pub length: f64, + pub diameter: f64, + pub liquid_flow: f64, + pub gas_flow: f64, + pub gas_fraction: f64, + pub axial_dispersion: f64, } +//TODO improve and change name pub struct Generator { raw_phase: Vec, } struct Field0D { + #[allow(unused)] name: String, + #[allow(unused)] value: f64, } +///wrapper Get absolute path from relative +fn resolve_path( + case: &CMCase, + relative_path: &str, + value: CMAExportType, +) -> Result { + case.resolve(relative_path, value) + .ok_or(CMError::Custom("Error resolving path".to_string())) +} -impl Generator { - fn f_write(f: impl RawData, r: &str, p: &str) -> Result<(), CmtoolError> { - f.write_raw(&format!("{}/{}", r, p))?; - Ok(()) - } +///Create vector of raw phase from raw +fn get_raw_phase( + flows: Vec, + vol: Vec, + phase: PhaseCM, +) -> Vec { + flows + .into_iter() + .zip(vol) + .map(|(f, v)| RawPhase { + flow: f, + volume: v, + identifier: phase, + }) + .collect() +} + +///Select specific phase type in a slice of phases +fn filter_phase(raw_phase: &[RawPhase], phase: PhaseCM) -> Vec { + raw_phase + .iter() + .filter(|p| p.identifier == phase) + .cloned() + .collect() +} +impl Generator { pub fn new() -> Self { Self { raw_phase: Default::default(), @@ -56,7 +86,7 @@ impl Generator { case: &mut CMCase, phase: RawPhase, relative_path: Option, - ) -> Result<(), CmtoolError> { + ) -> Result<(), CMError> { let (flowp, volumep) = phase.write(dest)?; let flowp = match &relative_path { Some(rel) => format!("./{}/{}", rel, flowp), @@ -79,7 +109,7 @@ impl Generator { volume: f64, phase: PhaseCM, dest: Option, - ) -> Result<(), CmtoolError> { + ) -> Result<(), CMError> { let mut phase = cmtool_data::RawPhase::new(1, 1, phase); phase.flow.fluxes[0] = Default::default(); //Not usefull because new already makes default phase.volume.values.push(volume.into()); @@ -99,7 +129,7 @@ impl Generator { gas_volume: f64, fields: Option<&[Field0D]>, dest: Option, - ) -> Result { + ) -> Result { let mut case = CMCase::default(); case.n_div = [1, 0, 0]; @@ -109,7 +139,7 @@ impl Generator { self.generate_0d_phase(&mut case, gas_volume, PhaseCM::Gas, dest)?; } - if let Some(scalars) = fields { + if let Some(_scalars) = fields { todo!("Scalar field") } @@ -121,7 +151,7 @@ impl Generator { total_volume: f64, gas_fraction: f64, dest: Option, - ) -> Result { + ) -> Result { if !(0. ..=1.).contains(&gas_fraction) { panic!("TODO: handle error gas fraction generation 0d"); } @@ -132,22 +162,23 @@ impl Generator { self.generate_0d(liquid_volume, gas_volume, None, dest) } + #[allow(clippy::too_many_arguments)] fn generate_1d( &mut self, case: &mut CMCase, n_compartment: usize, length: f64, diameter: f64, - liquid_flow: f64, + flow: f64, volume_fraction: f64, axial_dispersion: f64, gas: bool, dest: Option, - ) -> Result<(), CmtoolError> { + ) -> Result<(), CMError> { let dx = length / (n_compartment as f64); let reactor_section_area = std::f64::consts::PI * diameter.powf(2.) / 4.; let compartment_volume = volume_fraction * dx * reactor_section_area; - let flow_velocity = liquid_flow / reactor_section_area; + let flow_velocity = flow / reactor_section_area; let n_flow = n_compartment - 1; let flow_source_target = reactor_section_area / dx * (flow_velocity + axial_dispersion); @@ -184,15 +215,18 @@ impl Generator { pub fn generate_1d_from_fraction( &mut self, - n_compartment: usize, - length: f64, - diameter: f64, - liquid_flow: f64, - gas_flow: f64, - gas_fraction: f64, - axial_dispersion: f64, + + PFRDescription { + n_compartment, + length, + diameter, + liquid_flow, + gas_flow, + gas_fraction, + axial_dispersion, + }: PFRDescription, dest: Option, - ) -> Result { + ) -> Result { let mut case = CMCase::default(); case.n_div = [0, 0, n_compartment as u32]; @@ -228,13 +262,12 @@ impl Generator { Ok(case) } - //TODO add result type fn merge_phase( // flows: Vec, // volumes: Vec phases: Vec, connections: Option, - ) -> Result { + ) -> Result { let mut phase = RawPhase::new(0, 0, phases[0].identifier); // let mut merge_phase_flow = RawDataFlux::new(0, 0); // @@ -279,26 +312,16 @@ impl Generator { &self, dest: &str, connections: Option<[RawDataFlux; 2]>, - ) -> Result<(), CmtoolError> { - let liquid_phase = self - .raw_phase - .iter() - .filter(|p| p.identifier == PhaseCM::Liquid) - .cloned() - .collect(); - - let gasphase: Vec<_> = self - .raw_phase - .iter() - .filter(|p| p.identifier == PhaseCM::Gas) - .cloned() - .collect(); + ) -> Result { + const MERGE_FOLDER_NAME: &str = "merged"; + let liquid_phase = filter_phase(&self.raw_phase, PhaseCM::Liquid); + let gasphase = filter_phase(&self.raw_phase, PhaseCM::Gas); - let path = format!("{}/merged", dest); - std::fs::create_dir_all(&path).unwrap(); //FIXME + let path = format!("{}/{}", dest, MERGE_FOLDER_NAME); + std::fs::create_dir_all(&path)?; //FIXME let mut case = CMCase::default(); // case.n_div = n_div; - let relative = Some(String::from("merged")); //TODO Clean this + let relative = Some(String::from(MERGE_FOLDER_NAME)); let liquid_connection = connections.as_ref().map(|c| c[0].clone()); let gas_connection = connections.as_ref().map(|c| c[1].clone()); @@ -310,10 +333,10 @@ impl Generator { let phase = Self::merge_phase(gasphase, gas_connection)?; Self::write_phase(&path, &mut case, phase, relative)?; } - - CMCaseJson::write_case(case.clone(), Path::new(&format!("{}/jcma_case", dest)))?; - cmtool_data::CCMCaseInfo::write_case(case, Path::new(&format!("{}/cma_case", dest)))?; - Ok(()) + let path = format!("{}/cma_case", dest); + CMCaseJson::write_case(case.clone(), Path::new(&path))?; + // cmtool_data::CCMCaseInfo::write_case(case, Path::new(&format!("{}/cma_case", dest)))?; + Ok(path) } pub fn merge( @@ -321,7 +344,7 @@ impl Generator { dest: &str, ids: &[String], connections: Option<[RawDataFlux; 2]>, - ) -> Result<(), CmtoolError> { + ) -> Result { let mut liquid_flows = Vec::with_capacity(ids.len()); let mut liquid_volumes = Vec::with_capacity(ids.len()); let mut gas_flows = Vec::with_capacity(ids.len()); @@ -329,31 +352,29 @@ impl Generator { let mut n_div = [0, 0, 0]; for id in ids.iter() { - let case = CMCaseJson::read_case(Path::new(&format!("{}/{}/cma_case", dest, id)))?; + let partial_case = + CMCaseJson::read_case(Path::new(&format!("{}/{}/cma_case", dest, id)))?; let relative_path = format!("{}/{}", dest, id); let (liquid_flow, liquid_volume) = PAIRS.0; - let path = case - .resolve(&relative_path, liquid_flow) - .ok_or(CmtoolError::Custom("Error resolve".to_string()))?; - let rf = RawDataFlux::read_raw(path) - .ok_or(CmtoolError::Custom("Error reading".to_string()))?; + + let path = resolve_path(&partial_case, &relative_path, liquid_flow)?; + let rf = + RawDataFlux::read_raw(path).ok_or(CMError::Custom("Error reading".to_string()))?; let n_zone = rf.header.n_zone as usize; liquid_flows.push(rf); - n_div[0] += case.n_div[0]; - n_div[1] += case.n_div[1]; - n_div[2] += case.n_div[2]; + n_div[0] += partial_case.n_div[0]; + n_div[1] += partial_case.n_div[1]; + n_div[2] += partial_case.n_div[2]; + + let path = resolve_path(&partial_case, &relative_path, liquid_volume)?; - let path = case - .resolve(&relative_path, liquid_volume) - .ok_or(CmtoolError::Custom("Error resolve".to_string()))?; let sc = RawDataScalar::read_raw(path) - .ok_or(CmtoolError::Custom("Error reading".to_string()))?; + .ok_or(CMError::Custom("Error reading".to_string()))?; liquid_volumes.push(sc); let (gas_flow, gas_volume) = PAIRS.1; - let path = case - .resolve(&relative_path, gas_flow) - .ok_or(CmtoolError::Custom("Error resolve".to_string()))?; + let path = resolve_path(&partial_case, &relative_path, gas_flow)?; + let rf = match RawDataFlux::read_raw(path) { Some(rf) => rf, None => { @@ -363,9 +384,7 @@ impl Generator { }; gas_flows.push(rf); - let path = case - .resolve(&relative_path, gas_volume) - .ok_or(CmtoolError::Custom("Error resolve".to_string()))?; + let path = resolve_path(&partial_case, &relative_path, gas_volume)?; let rs = match RawDataScalar::read_raw(path) { Some(rs) => rs, None => { @@ -378,29 +397,12 @@ impl Generator { } let path = format!("{}/merged", dest); - std::fs::create_dir_all(&path).unwrap(); //FIXME + std::fs::create_dir_all(&path)?; let mut case = CMCase::default(); case.n_div = n_div; - let liquid_phases = liquid_flows - .into_iter() - .zip(liquid_volumes) - .map(|(f, v)| RawPhase { - flow: f, - volume: v, - identifier: PhaseCM::Liquid, - }) - .collect(); - - let gas_phases: Vec<_> = gas_flows - .into_iter() - .zip(gas_volumes) - .map(|(f, v)| RawPhase { - flow: f, - volume: v, - identifier: PhaseCM::Gas, - }) - .collect(); + let liquid_phases = get_raw_phase(liquid_flows, liquid_volumes, PhaseCM::Liquid); + let gas_phases = get_raw_phase(gas_flows, gas_volumes, PhaseCM::Gas); let liquid_connection = connections.as_ref().map(|c| c[0].clone()); let gas_connection = connections.as_ref().map(|c| c[1].clone()); @@ -416,7 +418,7 @@ impl Generator { CMCaseJson::write_case(case.clone(), Path::new(&format!("{}/jcma_case", dest)))?; let _ = cmtool_data::CCMCaseInfo::write_case(case, Path::new(&format!("{}/cma_case", dest))); - Ok(()) + Ok(path) } } @@ -458,12 +460,23 @@ mod tests { let l = 1.; let d = 0.2; let alpha_g = 0.1; + + let desc = PFRDescription { + n_compartment: 10, + length: l, + diameter: d, + liquid_flow: 0.01, + gas_flow: 0.01, + gas_fraction: alpha_g, + axial_dispersion: 1e-9, + }; + let case = Generator::new() - .generate_1d_from_fraction(10, l, d, 0.01, 0.01, alpha_g, 1e-9, Some(path.to_owned())) + .generate_1d_from_fraction(desc, Some(path.to_owned())) .expect("case"); - let liquid_volume_path: String = case - .resolve(path, cmtool_data::CMAExportType::LiquidVolume) - .expect("path"); + + let liquid_volume_path = + resolve_path(&case, path, cmtool_data::CMAExportType::LiquidVolume).unwrap(); let liquid_volume: f64 = cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()) .expect("Liquid error") @@ -485,8 +498,19 @@ mod tests { let l = 1.; let d = 0.2; let alpha_g = 0.1; + + let desc = PFRDescription { + n_compartment: 10, + length: l, + diameter: d, + liquid_flow: 0.01, + gas_flow: 0.001, + gas_fraction: alpha_g, + axial_dispersion: 1e-9, + }; + let case = Generator::new() - .generate_1d_from_fraction(10, l, d, 0.01, 0.001, alpha_g, 1e-9, Some(path.to_owned())) + .generate_1d_from_fraction(desc, Some(path.to_owned())) .expect("case"); let liquid_volume_path = case .resolve(path, cmtool_data::CMAExportType::LiquidVolume) diff --git a/cmtool-assemble/src/lib.rs b/cmtool-assemble/src/lib.rs new file mode 100644 index 00000000..731ab842 --- /dev/null +++ b/cmtool-assemble/src/lib.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +mod data; +mod generators; +mod map_generation; +mod parser; + +use std::io; + +pub use crate::data::{DomainData, DomainInfo}; +use crate::parser::{generated_domain::RootElementType, get_root, parse_domain}; +pub use data::{FeedFlow, ParsedFeeds}; +use map_generation::generate_flowmap; +use thiserror::Error; + +pub use cmtool_data::PhaseCM; //reexport to have easier dependency + +#[derive(Error, Debug)] +pub enum CMError { + #[error("Cmtool encountered an unknown error. Please check the input and try again.")] + Default, + + #[error("Cmtool error: {0}")] + Custom(String), + + #[error( + "Cmtool: Mass balance error in PFR '{0}' with phase {1}. Check your inputs or calculations." + )] + MassBalance(String, String), + + #[error("Cmtool data error: {0}. Ensure your data files are correct and accessible.")] + Data(#[from] cmtool_data::DataError), + + #[error("I/O error: Failed to handle the file '{0}'. Check file path and permissions.")] + IO(#[from] io::Error), + + #[error( + "Cmtool parse error: {0}. Verify that the XML input is well-formed and matches expected schema." + )] + Parse(#[from] serde_xml_rs::Error), +} + +/// Domain parser +// TODO: make it private + change name +pub struct Parser(RootElementType); + +impl Parser { + /// Create parser from xml content + /// Returns id + object if suceeds + pub fn start_parsing(reactor_content: &str) -> Result<(String, Self), CMError> { + let root = get_root(reactor_content)?; + Ok((root.run_id.clone(), Self(root))) + } + + /// Parse domain and generate content in the root directory if needed + /// Returns info about generated domain if suceeds + fn continue_parsing(p: Parser, root_dir: &str) -> Result { + // let (domain, mb) = parse_domain(&root)?; + // let root_dir = format!("{}/{}", root_dir, root.run_id); + // std::fs::create_dir_all(root_dir.clone())?; + // generate_flowmap(&root_dir, &domain, &root.reactors, &mb)?; + let path = format!("{}/{}", root_dir, p.0.run_id); + // Ok(domain) + Self::continue_parsing_with_path(p, path.as_str()) + } + + /// Parse domain and generate content at given abolute path if needed + /// Returns info about generated domain if suceeds + pub fn continue_parsing_with_path( + Parser(root): Parser, + root_dir: &str, + ) -> Result { + let (mut domain, mb) = parse_domain(&root)?; + + let path = if let Some(cm_case) = &domain.info.cm_case_only { + cm_case.clone() + } else { + //TODO: Do not create all, return error if not root_dir + std::fs::create_dir_all(root_dir)?; + generate_flowmap(root_dir, &domain, &root.reactors, &mb)? + }; + + domain.case_path = path; + Ok(domain) + } +} + +//Parse and generate domain at root dir from give xml content +// Returns info about domain if suceeds +pub fn generate_domain(root_dir: &str, reactor_content: &str) -> Result { + let (_id, root) = Parser::start_parsing(reactor_content)?; + + let domain = Parser::continue_parsing(root, root_dir)?; + + Ok(domain) +} + +//Parse and generate domain at root dir from give xml filepath +// For headless/cli use reads/create folder and generate +pub fn headless_generate( + reactor_input_file_name: &str, + path: impl AsRef, +) -> Result<(), CMError> { + let contents = std::fs::read_to_string(reactor_input_file_name)?; + //Useless because already created in continue_parsing_with_path + //TODO: keep it here and returns errors if not exist + let root_dir = path.as_ref().to_str().expect("path str").to_owned(); + std::fs::create_dir_all(&root_dir)?; + generate_domain(&root_dir, &contents)?; + Ok(()) +} diff --git a/cmtool-assemble/src/map_generation.rs b/cmtool-assemble/src/map_generation.rs new file mode 100644 index 00000000..0a4fea47 --- /dev/null +++ b/cmtool-assemble/src/map_generation.rs @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::CMError; +use crate::data::DomainData; +use crate::generators::{Generator, PFRDescription}; +use crate::parser::generated_domain::{self, GeneralSizeType}; +use crate::parser::{PfrGlobalMassBalance, generated_domain::Reactor0DType}; +use cmtool_data::{CMCaseJson, CMCaseReader, PhaseCM}; +use cmtool_data::{CMCaseWriter, DataError}; + +impl GeneralSizeType { + ///Returns volume of reactor considering cylindrical shape + pub fn get_volume(&self) -> f64 { + match self { + generated_domain::GeneralSizeType::Volume(v) => v.content as f64, + generated_domain::GeneralSizeType::Dimension(dim) => { + (dim.length.content as f64) + * (dim.diameter.content.powf(2.) as f64) + * std::f64::consts::PI + / 4. + } + } + } +} + +fn _generate_reactor_0d( + save_intermediate: bool, + generator: &mut Generator, + root: &str, + ids: &mut Vec, + reactor0d: &Reactor0DType, +) -> Result<(), CMError> { + ids.push(reactor0d.id.clone()); + let volume = reactor0d.size.get_volume(); + let path = format!("{}/{}", root, reactor0d.id); + + let mut opt_path = None; + if save_intermediate { + std::fs::create_dir_all(path.clone()).map_err(DataError::IO)?; + + opt_path = Some(path.clone()); + } + + let case = generator.generate_0d_from_fraction( + volume, + reactor0d.volume_fraction.content as f64, + opt_path, + )?; + + if save_intermediate { + T::write_case(case, std::path::Path::new(&format!("{}/cma_case", path)))?; + } + + Ok(()) +} + +fn _generate_reactor_1d( + save_intermediate: bool, + generator: &mut Generator, + root: &str, + ids: &mut Vec, + current_pfr: &generated_domain::Reactor1DType, + mb: &PfrGlobalMassBalance, +) -> Result<(), CMError> { + ids.push(current_pfr.id.clone()); + let path = format!("{}/{}", root, current_pfr.id); + + let mut opt_path = None; + if save_intermediate { + std::fs::create_dir_all(path.clone()).map_err(DataError::IO)?; + opt_path = Some(path.clone()); + } + + match ¤t_pfr.size { + generated_domain::GeneralSizeType::Volume(_) => { + unimplemented!("pfr needs length") + } + generated_domain::GeneralSizeType::Dimension(dim) => { + eprintln!("TODO: PFR GENERATION W/O FLOW RATES"); + + let desc = PFRDescription { + n_compartment: current_pfr.compartments.get(), + length: dim.length.content.into(), + diameter: dim.diameter.content.into(), + liquid_flow: mb.get_flow(¤t_pfr.id, PhaseCM::Liquid)?, + gas_flow: mb.get_flow(¤t_pfr.id, PhaseCM::Gas)?, + gas_fraction: current_pfr.volume_fraction.content as f64, + axial_dispersion: 1e-9, + }; + + let case: cmtool_data::CMCase = generator.generate_1d_from_fraction(desc, opt_path)?; + if save_intermediate { + T::write_case(case, std::path::Path::new(&format!("{}/cma_case", path)))?; + } + } + }; + Ok(()) +} + +fn generate_partial_flowmap( + save_intermediate: bool, + generator: &mut Generator, + root: &str, + reactors: &generated_domain::ReactorsType, + mb: &PfrGlobalMassBalance, +) -> Result, CMError> { + let mut ids = Vec::with_capacity(reactors.content.len()); + + for reactor in &reactors.content { + match reactor { + generated_domain::ReactorsTypeContent::Reactor0D(r) => { + _generate_reactor_0d::(save_intermediate, generator, root, &mut ids, r)?; + } + generated_domain::ReactorsTypeContent::Reactor1D(r) => { + _generate_reactor_1d::(save_intermediate, generator, root, &mut ids, r, mb)?; + } + generated_domain::ReactorsTypeContent::Reactor3D(reactor3_dtype) => { + todo!("{:?}", reactor3_dtype) + } + generated_domain::ReactorsTypeContent::ReactorFromFile(_) => { + todo!("generate_partial_flowmap::ReactorFromFile") + } + } + } + Ok(ids) +} + +pub fn generate_flowmap( + root: &str, + domain: &DomainData, + reactors: &generated_domain::ReactorsType, + mb: &PfrGlobalMassBalance, +) -> Result { + let mut generator = Generator::new(); + + let save_intermediate = reactors.content.len() == 1; + + let _ids = generate_partial_flowmap::( + save_intermediate, + &mut generator, + root, + reactors, + mb, + )?; + //TODO remove returning cm_path merge and merge_from_memory dont need to return it cause it is 'root' + //Same when len(id)==1 + if _ids.len() > 1 { + if save_intermediate { + generator.merge(root, &_ids, domain.connections.clone())?; + } else { + generator.merge_from_memory(root, domain.connections.clone())?; + } + Ok(root.to_owned()) + } else if _ids.len() == 1 && save_intermediate { + let case_path = format!("{}/{}/cma_case", root, _ids[0]); + let prep = format!("./{}", _ids[0]); + let case = CMCaseJson::read_case(std::path::Path::new(&case_path))?.prepend_path(&prep); + let path = format!("{}/cma_case", root); + CMCaseJson::write_case(case, std::path::Path::new(&path))?; + Ok(root.to_owned()) + } else { + Err(CMError::Custom("TODO ".to_owned())) + } +} + +// fn parse_generate() +// {} diff --git a/cmtool-assemble/src/parser/generated_domain.rs b/cmtool-assemble/src/parser/generated_domain.rs new file mode 100644 index 00000000..4875a736 --- /dev/null +++ b/cmtool-assemble/src/parser/generated_domain.rs @@ -0,0 +1,5 @@ + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(unused_imports)] +use serde :: { Deserialize , Serialize } ; pub type Root = RootElementType ; # [derive (Clone , Debug , Deserialize , Serialize)] pub struct RootElementType { # [serde (rename = "@run_id")] pub run_id : :: std :: string :: String , # [serde (rename = "@version")] pub version : :: core :: primitive :: i32 , # [serde (rename = "Reactors")] pub reactors : ReactorsType , # [serde (default , rename = "Connections")] pub connections : :: core :: option :: Option < ConnectionsType > , # [serde (default , rename = "Feeds")] pub feeds : :: core :: option :: Option < FeedsType > , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct DimensionType { # [serde (default , rename = "@unit")] pub unit : :: core :: option :: Option < :: std :: string :: String > , # [serde (rename = "#text")] pub content : :: core :: primitive :: f32 , } pub type FlowType = DimensionType ; pub type LengthType = DimensionType ; # [derive (Clone , Debug , Deserialize , Serialize)] pub enum SizeSpecificationType { # [serde (rename = "Volume")] Volume (DimensionType) , # [serde (rename = "CylindricalDimensions")] CylindricalDimensions (SizeSpecificationCylindricalDimensionsElementType) , } pub type VolumeType = DimensionType ; # [derive (Clone , Debug , Deserialize , Serialize)] pub struct AutoFeedType { # [serde (rename = "@phase")] pub phase : :: std :: string :: String , # [serde (rename = "D")] pub d : NodeType , # [serde (rename = "N")] pub n : NodeType , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct ConnectionsType { # [serde (default , rename = "Flux")] pub flux : :: std :: vec :: Vec < FluxType > , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct FeedFluxType { # [serde (rename = "@phase")] pub phase : :: std :: string :: String , # [serde (rename = "@id")] pub id : :: std :: string :: String , # [serde (rename = "Source")] pub source : NodeType , # [serde (rename = "Target")] pub target : NodeType , # [serde (rename = "Value")] pub value : DimensionType , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct FeedsType { # [serde (default , rename = "Flux")] pub flux : :: std :: vec :: Vec < FeedFluxType > , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct FluxType { # [serde (rename = "@phase")] pub phase : :: std :: string :: String , # [serde (rename = "Source")] pub source : NodeType , # [serde (rename = "Target")] pub target : NodeType , # [serde (rename = "Value")] pub value : DimensionType , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct NodeType { # [serde (rename = "@id")] pub id : :: std :: string :: String , # [serde (rename = "@compartment_id")] pub compartment_id : :: core :: primitive :: usize , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct BaseReactorType { # [serde (rename = "@id")] pub id : :: std :: string :: String , # [serde (rename = "VolumeFraction")] pub volume_fraction : VolumeFractionType , # [serde (rename = "Size")] pub size : GeneralSizeType , } # [derive (Clone , Debug , Deserialize , Serialize)] pub enum GeneralSizeType { # [serde (rename = "Volume")] Volume (DimensionType) , # [serde (rename = "Dimension")] Dimension (SizeCylindricalType) , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct Reactor0DType { # [serde (rename = "@id")] pub id : :: std :: string :: String , # [serde (rename = "VolumeFraction")] pub volume_fraction : VolumeFractionType , # [serde (rename = "Size")] pub size : GeneralSizeType , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct Reactor1DType { # [serde (rename = "@id")] pub id : :: std :: string :: String , # [serde (rename = "VolumeFraction")] pub volume_fraction : VolumeFractionType , # [serde (rename = "Size")] pub size : GeneralSizeType , # [serde (rename = "Compartments")] pub compartments : :: core :: num :: NonZeroUsize , # [serde (rename = "Dispersion")] pub dispersion : DimensionType , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct Reactor3DType { # [serde (rename = "@id")] pub id : :: std :: string :: String , # [serde (rename = "VolumeFraction")] pub volume_fraction : VolumeFractionType , # [serde (rename = "Size")] pub size : GeneralSizeType , # [serde (rename = "file")] pub file : :: std :: string :: String , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct ReactorFromFileType { # [serde (rename = "@id")] pub id : :: std :: string :: String , # [serde (rename = "Path")] pub path : :: std :: string :: String , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct ReactorsType { # [serde (rename = "#content")] pub content : :: std :: vec :: Vec < ReactorsTypeContent > , } # [derive (Clone , Debug , Deserialize , Serialize)] pub enum ReactorsTypeContent { # [serde (rename = "Reactor0D")] Reactor0D (Reactor0DType) , # [serde (rename = "Reactor1D")] Reactor1D (Reactor1DType) , # [serde (rename = "Reactor3D")] Reactor3D (Reactor3DType) , # [serde (rename = "ReactorFromFile")] ReactorFromFile (ReactorFromFileType) , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct SizeCylindricalType { # [serde (rename = "Diameter")] pub diameter : DimensionType , # [serde (rename = "Length")] pub length : DimensionType , } # [derive (Clone , Debug , Deserialize , Serialize)] pub struct VolumeFractionType { # [serde (default , rename = "@phase")] pub phase : :: core :: option :: Option < :: std :: string :: String > , # [serde (rename = "#text")] pub content : :: core :: primitive :: f32 , } # [derive (Clone , Debug , Default , Deserialize , Serialize)] pub struct EntitiesType (pub :: std :: vec :: Vec < :: std :: string :: String >) ; pub type EntityType = :: std :: string :: String ; pub type IdType = :: std :: string :: String ; pub type IdrefType = :: std :: string :: String ; pub type IdrefsType = EntitiesType ; pub type NcNameType = :: std :: string :: String ; pub type NmtokenType = :: std :: string :: String ; pub type NmtokensType = EntitiesType ; pub type NotationType = :: std :: string :: String ; pub type NameType = :: std :: string :: String ; pub type QNameType = :: std :: string :: String ; # [derive (Clone , Debug , Deserialize , Serialize)] pub struct AnySimpleType { # [serde (default , rename = "@xsi:type")] pub type_ : :: core :: option :: Option < :: std :: string :: String > , # [serde (default , rename = "#text")] pub content : :: std :: string :: String , } pub type AnyUriType = :: std :: string :: String ; pub type Base64BinaryType = :: std :: string :: String ; pub type BooleanType = :: core :: primitive :: bool ; pub type ByteType = :: core :: primitive :: i8 ; pub type DateType = :: std :: string :: String ; pub type DateTimeType = :: std :: string :: String ; pub type DecimalType = :: core :: primitive :: f64 ; pub type DoubleType = :: core :: primitive :: f64 ; pub type DurationType = :: std :: string :: String ; pub type FloatType = :: core :: primitive :: f32 ; pub type GDayType = :: std :: string :: String ; pub type GMonthType = :: std :: string :: String ; pub type GMonthDayType = :: std :: string :: String ; pub type GYearType = :: std :: string :: String ; pub type GYearMonthType = :: std :: string :: String ; pub type HexBinaryType = :: std :: string :: String ; pub type IntType = :: core :: primitive :: i32 ; pub type IntegerType = :: core :: primitive :: i32 ; pub type LanguageType = :: std :: string :: String ; pub type LongType = :: core :: primitive :: i64 ; pub type NegativeIntegerType = :: core :: num :: NonZeroIsize ; pub type NonNegativeIntegerType = :: core :: primitive :: usize ; pub type NonPositiveIntegerType = :: core :: primitive :: isize ; pub type NormalizedStringType = :: std :: string :: String ; pub type PositiveIntegerType = :: core :: num :: NonZeroUsize ; pub type ShortType = :: core :: primitive :: i16 ; pub type StringType = :: std :: string :: String ; pub type TimeType = :: std :: string :: String ; pub type TokenType = :: std :: string :: String ; pub type UnsignedByteType = :: core :: primitive :: u8 ; pub type UnsignedIntType = :: core :: primitive :: u32 ; pub type UnsignedLongType = :: core :: primitive :: u64 ; pub type UnsignedShortType = :: core :: primitive :: u16 ; # [derive (Clone , Debug , Deserialize , Serialize)] pub struct SizeSpecificationCylindricalDimensionsElementType { # [serde (rename = "Diameter")] pub diameter : DimensionType , # [serde (rename = "Length")] pub length : DimensionType , } \ No newline at end of file diff --git a/cmtool-assemble/src/parser/mod.rs b/cmtool-assemble/src/parser/mod.rs new file mode 100644 index 00000000..b3e5ce3b --- /dev/null +++ b/cmtool-assemble/src/parser/mod.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#[rustfmt::skip] +pub mod generated_domain; +use crate::{CMError, DomainData}; +mod reactors; +use reactors::{parse_connection, parse_feed, parse_reactor}; +mod pfr_mb; +pub(super) use pfr_mb::PfrGlobalMassBalance; + +pub fn parse_domain( + root: &generated_domain::RootElementType, +) -> Result<(DomainData, PfrGlobalMassBalance), CMError> { + if root.reactors.content.is_empty() { + return Err(CMError::Parse(serde_xml_rs::Error::Custom( + "At least one reactor required".to_owned(), + ))); + } + + let info = parse_reactor(&root.reactors)?; + + let mut mass_balance = PfrGlobalMassBalance::new(&info.pfr_names); + + let raw_connections = root + .connections + .as_ref() + .map(|connections| parse_connection(&info, connections, &mut mass_balance)); + + let mut pfeeds = None; + if let Some(feeds) = &root.feeds { + pfeeds = parse_feed(&info, feeds, &mut mass_balance); + } + mass_balance.validate()?; + let run_id = root.run_id.clone(); + Ok(( + DomainData { + connections: raw_connections, + info, + feeds: pfeeds, + case_path: String::new(), + run_id, + }, + mass_balance, + )) +} + +pub fn get_root(content: &str) -> Result { + let root = serde_xml_rs::from_str::(content)?; + eprintln!("WARNING: Some reactor may miss if xml is not parsed correctly"); + if root.version != 3 { + panic!("ALED"); + } + Ok(root) +} diff --git a/cmtool-assemble/src/parser/pfr_mb.rs b/cmtool-assemble/src/parser/pfr_mb.rs new file mode 100644 index 00000000..a413d527 --- /dev/null +++ b/cmtool-assemble/src/parser/pfr_mb.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::{CMError, data::FlowDirection}; +use cmtool_data::PhaseCM; +use std::collections::HashMap; + +///Input and output flow for one phase in one pfr +#[derive(Default, Copy, Clone, Debug)] +struct FlowData { + in_flow: f64, + out_flow: f64, +} + +/// flow data for one pfr +#[derive(Copy, Clone, Debug, Default)] +pub struct PhaseFlow { + gas: FlowData, + liquid: FlowData, +} + +/// Struct to hold flow data for each ID using a HashMap +#[derive(Default, Debug)] +pub struct PfrGlobalMassBalance { + flows: HashMap, + validated: bool, +} + +impl PfrGlobalMassBalance { + pub(super) fn new(pfr_names: &[String]) -> Self { + let mut flows = HashMap::new(); + for name in pfr_names { + flows.insert(name.clone(), PhaseFlow::default()); + } + PfrGlobalMassBalance { + flows, + validated: false, + } + } + + pub(super) fn validate(&mut self) -> Result<(), CMError> { + for (i, flow) in &self.flows { + if flow.gas.in_flow != flow.gas.out_flow { + return Err(CMError::MassBalance(i.clone(), "gas".to_owned())); + } + if flow.liquid.in_flow != flow.liquid.out_flow { + return Err(CMError::MassBalance(i.clone(), "liquid".to_owned())); + } + } + self.validated = true; + Ok(()) + } + + pub(crate) fn update_flow( + &mut self, + id: &str, + phase: PhaseCM, + direction: FlowDirection, + vflow: f64, + ) { + self.validated = false; + if let Some(phase_flow) = self.flows.get_mut(id) { + let flow_data = match phase { + PhaseCM::Gas => &mut phase_flow.gas, + PhaseCM::Liquid => &mut phase_flow.liquid, + }; + + match direction { + FlowDirection::In => flow_data.in_flow += vflow, + FlowDirection::Out => flow_data.out_flow += vflow, + } + } + } + pub fn get_flow(&self, id: &str, phase: PhaseCM) -> Result { + if !self.validated { + return Err(CMError::Custom( + "Mass balance needs to be validated before being accessed".to_owned(), + )); + } + + if let Some(phase_flow) = self.flows.get(id) { + return Ok(match phase { + PhaseCM::Gas => phase_flow.gas.in_flow, + PhaseCM::Liquid => phase_flow.liquid.in_flow, + }); + } + Err(CMError::Custom(format!( + "Mass balance does not provide {} pfr", + id + ))) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_pfr_mass_balance() { + let names = ["name1".to_owned(), "name2".to_owned()]; + let mut mb = PfrGlobalMassBalance::new(&names); + + let expected_ok = mb.validate(); //BEcause flow is null + assert!(expected_ok.is_ok()); + + mb.update_flow("name1", PhaseCM::Gas, FlowDirection::In, 4.); + let expected_err = mb.validate(); + assert!(expected_err.is_err()); + + mb.update_flow("name1", PhaseCM::Gas, FlowDirection::Out, 2.); + mb.update_flow("name1", PhaseCM::Gas, FlowDirection::Out, 2.); + + let expected_ok = mb.validate(); + assert!(expected_ok.is_ok()); + + let pfr_flow = mb.get_flow("name1", PhaseCM::Gas).unwrap(); + assert!(pfr_flow == 4.) + } +} diff --git a/cmtool-assemble/src/parser/reactors.rs b/cmtool-assemble/src/parser/reactors.rs new file mode 100644 index 00000000..1238cc7c --- /dev/null +++ b/cmtool-assemble/src/parser/reactors.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::collections::HashMap; +use std::path::PathBuf; + +use super::PfrGlobalMassBalance; +use super::generated_domain; +use crate::data::FeedFlow; +use crate::data::ParsedFeeds; +use crate::parser::generated_domain::FeedFluxType; +use crate::parser::generated_domain::FluxType; +use crate::{ + CMError, + data::{DomainInfo, FlowDirection}, +}; +use cmtool_data::{PhaseCM, RawDataFlux}; + +fn connection_per_phase( + info: &DomainInfo, + mass_balance: &mut PfrGlobalMassBalance, + phase_node: &[generated_domain::FluxType], + phase: PhaseCM, +) -> cmtool_data::RawDataFlux { + let n_node = phase_node.len(); + + let mut rd = RawDataFlux::new(info.total_number_compartment, n_node); + + for (node, flux) in phase_node.iter().zip(rd.fluxes.iter_mut()) { + if let (Some(absolute_source_id), Some(absolute_target_id)) = ( + info.get_relative_compartment_number(&node.source.id, node.source.compartment_id), + info.get_relative_compartment_number(&node.target.id, node.target.compartment_id), + ) { + let flow = f64::from(node.value.content); + flux.id_source = absolute_source_id as u32; + flux.id_target = absolute_target_id as u32; + flux.flux_source_target = flow; + flux.flux_target_source = 0.; //Connection is pure PlugFLow + + let f_pfr_source = info.pfr_names.contains(&node.source.id); + let is_same_node = + absolute_source_id == absolute_target_id && node.source.id == node.target.id; + + if is_same_node && f_pfr_source { + mass_balance.update_flow(&node.target.id, phase, FlowDirection::Out, flow); + } else { + let f_pfr_target = info.pfr_names.contains(&node.target.id); + if f_pfr_target { + mass_balance.update_flow(&node.target.id, phase, FlowDirection::In, flow); + } + if f_pfr_source { + mass_balance.update_flow(&node.source.id, phase, FlowDirection::Out, flow); + } + } + } else { + eprintln!( + "Ignored connection src:{} {}", + node.source.id, node.target.id + ); + panic!("TODO: Handle error when flux doesnt work") + } + } + rd +} + +pub fn parse_connection( + info: &DomainInfo, + connections: &generated_domain::ConnectionsType, + mass_balance: &mut PfrGlobalMassBalance, +) -> [RawDataFlux; 2] { + let liquid_connection: Vec = connections + .flux + .iter() + .filter(|f| f.phase == *"liquid") + .cloned() + .collect(); + + let gas_connection: Vec = connections + .flux + .iter() + .filter(|f| f.phase == *"gas") + .cloned() + .collect(); + + [ + connection_per_phase(info, mass_balance, &liquid_connection, PhaseCM::Liquid), + connection_per_phase(info, mass_balance, &gas_connection, PhaseCM::Gas), + ] +} + +///Flux type is a "derivated" type of flux with all flux information + the flow value +///Current implementation performs naive copy of common attributes. +impl From<&FeedFluxType> for FluxType { + fn from(feed: &FeedFluxType) -> Self { + Self { + source: feed.source.clone(), + target: feed.target.clone(), + phase: feed.phase.clone(), + value: feed.value.clone(), + } + } +} + +fn parse_feed_phase( + info: &DomainInfo, + feeds: &[&FeedFluxType], + phase: PhaseCM, + mass_balance: &mut PfrGlobalMassBalance, +) -> HashMap { + let fluxes: Vec = + feeds.iter().map(|&feed| FluxType::from(feed)).collect(); + + let id: Vec = feeds.iter().map(|feed| feed.id.clone()).collect(); + let rd = connection_per_phase(info, mass_balance, &fluxes, phase); + + rd.fluxes + .iter() + .zip(id) + .map(|(flux, id)| { + ( + id, + FeedFlow { + flow: flux.flux_source_target, + position: flux.id_source as usize, + output_position: None, //TODO + }, + ) + }) + .collect() +} + +pub fn parse_feed( + info: &DomainInfo, + feeds: &generated_domain::FeedsType, + mass_balance: &mut PfrGlobalMassBalance, +) -> Option { + let (liquid_feeds, gas_feeds): (Vec<_>, Vec<_>) = feeds + .flux + .iter() + .partition(|f| f.phase == *"liquid") + .clone(); + if liquid_feeds.is_empty() && gas_feeds.is_empty() { + return None; + } + Some(ParsedFeeds { + liq: parse_feed_phase(info, &liquid_feeds, PhaseCM::Liquid, mass_balance), + gas: parse_feed_phase(info, &gas_feeds, PhaseCM::Gas, mass_balance), + }) +} + +pub fn parse_reactor(reactors: &generated_domain::ReactorsType) -> Result { + let mut domain_info = DomainInfo::default(); + let mut cm_case_only = None; + let mut in_place_cumsum = 0; + + for reactor in &reactors.content { + match reactor { + generated_domain::ReactorsTypeContent::Reactor0D(reactor0_dtype) => { + domain_info + .compartment_cumsum + .insert(reactor0_dtype.id.clone(), in_place_cumsum); + domain_info.total_number_compartment += 1; + in_place_cumsum += 1; + domain_info.is_two_phase_flow = reactor0_dtype.volume_fraction.content != 0.; + } + generated_domain::ReactorsTypeContent::Reactor1D(current_pfr) => { + domain_info + .compartment_cumsum + .insert(current_pfr.id.clone(), in_place_cumsum); + let n_c = current_pfr.compartments; + domain_info.total_number_compartment += n_c.get(); + domain_info.is_two_phase_flow = current_pfr.volume_fraction.content != 0.; + domain_info.pfr_names.push(current_pfr.id.clone()); + in_place_cumsum += n_c.get(); + } + generated_domain::ReactorsTypeContent::ReactorFromFile(reactor_from_file) => { + let path = format!("{}/cma_case", reactor_from_file.path); + + let path = PathBuf::from(path); + let case = cmtool_data::read_case(path.as_path())?; + // case.n_compartment() + domain_info + .compartment_cumsum + .insert(reactor_from_file.id.clone(), in_place_cumsum); + let n_c = case.n_compartment() as usize; + domain_info.is_two_phase_flow = case.is_two_phase_flow(); + domain_info.total_number_compartment += n_c; + in_place_cumsum += n_c; + + if cm_case_only.is_none() { + cm_case_only = Some(reactor_from_file.path.clone()); + } else { + todo!("Existing flowmap merge") + } + println!("{:?}", cm_case_only); + } + generated_domain::ReactorsTypeContent::Reactor3D(reactor3_dtype) => { + todo!("{:?}", reactor3_dtype) + } + } + } + domain_info.cm_case_only = cm_case_only; + Ok(domain_info) +} diff --git a/cmtool-core/src/coordinates/mod.rs b/cmtool-core/src/coordinates/mod.rs index 5199a5c8..ee24e8c2 100644 --- a/cmtool-core/src/coordinates/mod.rs +++ b/cmtool-core/src/coordinates/mod.rs @@ -8,19 +8,6 @@ pub use vec3::*; pub const NUMBER_OF_AXIS: usize = 3; pub type Coords3 = [f64; NUMBER_OF_AXIS]; -pub struct Plane { - pub normal: CartesianVec3, - pub point: CartesianCoordinates, -} - -pub struct BoundedPlane { - pub normal: CartesianVec3, - pub origin: CartesianCoordinates, - pub extent_u: [f64; 2], - pub extent_v: [f64; 2], - pub axis: usize, // 0 = r, 1 = theta, 2 = z -} - pub fn orthonormal_basis(normal: &CartesianVec3) -> (CartesianVec3, CartesianVec3) { let n = normal.normalized(); let temp = if n.0[0].abs() < 0.9 { @@ -34,46 +21,57 @@ pub fn orthonormal_basis(normal: &CartesianVec3) -> (CartesianVec3, CartesianVec (u, v) } -impl BoundedPlane { - pub fn is_point_inside(&self, CartesianCoordinates(point): CartesianCoordinates) -> bool { - // let v = CartesianVec3([ - // point[0] - self.origin.0[0], - // point[1] - self.origin.0[1], - // point[2] - self.origin.0[2], - // ]); - - // let (u_dir, v_dir) = orthonormal_basis(&self.normal); - - // let u_proj = v.dot(&u_dir); - // let v_proj = v.dot(&v_dir); +pub struct Plane { + pub normal: CartesianVec3, + pub point: CartesianCoordinates, +} - // (u_proj >= self.extent_u[0] && u_proj <= self.extent_u[1]) - // && (v_proj >= self.extent_v[0] && v_proj <= self.extent_v[1]) +pub struct BoundedPlane { + pub normal: CartesianVec3, + pub origin: CartesianCoordinates, + pub extent_u: [f64; 2], + pub extent_v: [f64; 2], + pub axis: usize, // 0 = r, 1 = theta, 2 = z +} +impl BoundedPlane { + pub fn is_point_inside(&self, CartesianCoordinates(point): CartesianCoordinates) -> bool { let r = (point[0].powi(2) + point[1].powi(2)).sqrt(); - let theta = point[1].atan2(point[0]); + let theta_raw = point[1].atan2(point[0]); let z = point[2]; + const TOL: f64 = 1e-10; let (u, v) = match self.axis { 0 => { - let u = theta; - let v = z; - (u, v) - } - 1 => { - let u = r; - let v = z; - (u, v) + let pi2 = 2.0 * std::f64::consts::PI; + let mid = 0.5 * (self.extent_u[0] + self.extent_u[1]); + let mut theta = theta_raw; + while theta - mid > std::f64::consts::PI { + theta -= pi2; + } + while mid - theta > std::f64::consts::PI { + theta += pi2; + } + (theta, z) } + 1 => (r, z), 2 => { - let u = r; - let v = theta; - (u, v) + let pi2 = 2.0 * std::f64::consts::PI; + let mid = 0.5 * (self.extent_v[0] + self.extent_v[1]); + let mut theta = theta_raw; + while theta - mid > std::f64::consts::PI { + theta -= pi2; + } + while mid - theta > std::f64::consts::PI { + theta += pi2; + } + (r, theta) } _ => unreachable!(), }; - (u >= self.extent_u[0] && u <= self.extent_u[1]) - && (v >= self.extent_v[0] && v <= self.extent_v[1]) + + (u >= self.extent_u[0] - TOL && u <= self.extent_u[1] + TOL) + && (v >= self.extent_v[0] - TOL && v <= self.extent_v[1] + TOL) } } @@ -86,6 +84,16 @@ impl From for Plane { } } +pub fn get_normal(axis: usize, negative: bool) -> Coords3 { + let sign = if negative { -1. } else { 1. }; + match axis { + 0 => [sign, 0.0, 0.0], + 1 => [0.0, sign, 0.0], + 2 => [0.0, 0.0, sign], + _ => unreachable!("Axis must be 0, 1, or 2"), + } +} + // pub trait AsCoords3 { // fn as_coords(&self) -> &Coords3; // } diff --git a/cmtool-core/src/coordinates/vec3.rs b/cmtool-core/src/coordinates/vec3.rs index 4fea7389..fecb3fc4 100644 --- a/cmtool-core/src/coordinates/vec3.rs +++ b/cmtool-core/src/coordinates/vec3.rs @@ -2,10 +2,10 @@ use crate::coordinates::{CartesianCoordinates, Coords3}; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct CartesianVec3(pub Coords3); -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct CylindricalVec3(pub Coords3, pub f64); pub trait Vec3 { @@ -168,7 +168,7 @@ mod tests { let cyl = cart.to_cylindrical_vec(base_theta); let vr_expected = 1.0 * base_theta.cos() + 0.0 * base_theta.sin(); - let vtheta_expected = -1.0 * base_theta.sin() + 0.0 * base_theta.cos(); + let vtheta_expected = -base_theta.sin() + 0.0 * base_theta.cos(); let vz_expected = 2.0; assert!((cyl.0[0] - vr_expected).abs() < 1e-10); @@ -240,6 +240,7 @@ mod tests { assert!((result - 5.0).abs() < 1e-10); } + #[allow(clippy::needless_range_loop)] #[test] fn test_normalized() { let v = CartesianVec3([0.0, 3.0, 4.0]); diff --git a/cmtool-core/src/ensight_gold/case.rs b/cmtool-core/src/ensight_gold/case.rs index c575c737..17d57afd 100644 --- a/cmtool-core/src/ensight_gold/case.rs +++ b/cmtool-core/src/ensight_gold/case.rs @@ -8,7 +8,7 @@ use std::{ use crate::CoreError; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum VariableType { Scalar, Vector, @@ -26,7 +26,7 @@ impl TryInto for String { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct VariableInfo { pub var_type: String, pub name: String, diff --git a/cmtool-core/src/ensight_gold/geo.rs b/cmtool-core/src/ensight_gold/geo.rs index 007d26d6..acc5f097 100644 --- a/cmtool-core/src/ensight_gold/geo.rs +++ b/cmtool-core/src/ensight_gold/geo.rs @@ -218,12 +218,15 @@ impl Geometry { reader.ignore_line()?; } - let node_id_choice = reader.get_line_string()?; + let (ignore_node_id, ignore_element_id) = { + let _node_id_choice = reader.get_line_string()?; - let ignore_node_id = false; //TODO find in node_id_choice: assign + let ignore_node_id = false; //TODO find in node_id_choice: assign - let element_id_choice = reader.get_line_string()?; - let ignore_element_id = false; //TODO find in element_id_choice: assign + let _element_id_choice = reader.get_line_string()?; + let ignore_element_id = false; //TODO find in element_id_choice: assign + (ignore_node_id, ignore_element_id) + }; // println!("{} {}", node_id_choice, element_id_choice); diff --git a/cmtool-core/src/errors.rs b/cmtool-core/src/errors.rs index 11b3f5da..f53fc741 100644 --- a/cmtool-core/src/errors.rs +++ b/cmtool-core/src/errors.rs @@ -2,6 +2,15 @@ use thiserror::Error; +#[derive(Error, Debug)] +pub enum ModelError { + #[error("Cell to cell divergence higher than tolerance: {0}>{1}")] + CellToCellDivergence(f64, f64), + + #[error("Resulting flow has invalid value ")] + InvalidFlow, +} + #[derive(Error, Debug)] pub enum CoreError { #[error("Cmtool: {0}")] @@ -15,4 +24,7 @@ pub enum CoreError { #[error("Error writing/reading file: {0}")] IO(#[from] std::io::Error), + + #[error("Model: {0}")] + Model(#[from] ModelError), } diff --git a/cmtool-core/src/grid/collections.rs b/cmtool-core/src/grid/collections.rs index f6abe85e..40189279 100644 --- a/cmtool-core/src/grid/collections.rs +++ b/cmtool-core/src/grid/collections.rs @@ -58,6 +58,7 @@ pub const fn cylindrical_index(axis: CylindricalAxis) -> usize { } #[inline(always)] +#[allow(unused)] //FIXME pub const fn index_to_oriented(axis: usize) -> OrientedAxis { match axis { 0 => OrientedAxis::I, diff --git a/cmtool-core/src/grid/mod.rs b/cmtool-core/src/grid/mod.rs index e1efd17e..fe44bfd3 100644 --- a/cmtool-core/src/grid/mod.rs +++ b/cmtool-core/src/grid/mod.rs @@ -2,7 +2,9 @@ mod collections; use collections::*; -pub use collections::{AxisDescriptor, CylindricalAxis, cylindrical_index}; +pub use collections::{ + AxisDescriptor, CylindricalAxis, OrientedAxis, cylindrical_index, index_to_oriented, +}; use enum_dispatch::enum_dispatch; use std::f64; @@ -12,30 +14,52 @@ pub(crate) mod vtk; use crate::coordinates::*; use crate::utils::AxisPoints; -fn get_tangent_plane_at_r( - axis: usize, - r0: f64, - theta: f64, - z: f64, - extent_u: [f64; 2], - extent_v: [f64; 2], -) -> BoundedPlane { - let x0 = r0 * theta.cos(); - let y0 = r0 * theta.sin(); - let z0 = z; - - let normal = CartesianVec3([x0 / r0, y0 / r0, 0.0]); - - let origin = CartesianCoordinates([x0, y0, z0]); - - BoundedPlane { - normal, - origin, - extent_u, // extensités sur theta - extent_v, // extensités sur z - axis, - } -} +// fn get_tangent_plane_at_r( +// axis: usize, +// r0: f64, +// theta: f64, +// z: f64, +// extent_u: [f64; 2], +// extent_v: [f64; 2], +// ) -> BoundedPlane { +// let x0 = r0 * theta.cos(); +// let y0 = r0 * theta.sin(); +// let z0 = z; + +// let normal = CartesianVec3([x0 / r0, y0 / r0, 0.0]); + +// let origin = CartesianCoordinates([x0, y0, z0]); + +// BoundedPlane { +// normal, +// origin, +// extent_u, +// extent_v, +// axis, +// } +// } +// +// fn get_tangent_plane_at_r( +// axis: usize, +// r0: f64, +// theta: f64, +// z: f64, +// extent_u: [f64; 2], // [theta0, theta1] — will be converted to arc length +// extent_v: [f64; 2], // [z0, z1] — already metric +// ) -> BoundedPlane { +// let origin = CartesianCoordinates([r0 * theta.cos(), r0 * theta.sin(), z]); +// let normal = CartesianVec3([theta.cos(), theta.sin(), 0.0]); + +// let extent_u_metric = [r0 * extent_u[0], r0 * extent_u[1]]; + +// BoundedPlane { +// normal, +// origin, +// extent_u: extent_u_metric, +// extent_v, // z is already metric +// axis, +// } +// } /// Represents the type of mesh geometry. #[derive(PartialEq, Clone, Copy)] @@ -227,7 +251,7 @@ pub trait CompartmentMeshManip { /// # Arguments /// /// * `cell_id` - The ID of the cell. - /// /// * `axis_project` - Axis index on which the surface is calculated + /// * `axis_project` - Axis index on which the surface is calculated /// /// # Returns /// @@ -287,6 +311,9 @@ pub trait CompartmentMeshManip { fn n_maximum_interface(&self) -> usize; fn get_interface_plane(&self, cell1_id: usize, cell2_id: usize) -> (BoundedPlane, usize); + + fn cell_from_ax_points(&self, axis_points: &AxisPoints) -> Option; + fn get_boundary(&self) -> Vec; } /// A compartment mesh grid. /// @@ -413,15 +440,10 @@ impl CompartmentMeshManip for MeshCylindrical { let neighbors = self.are_cell_neighbor(cell1_id, cell2_id); let axis = neighbors .to_coord_index() - .expect("Cells must be neighbors to get interface plane"); + .expect("RMTOOL(get_interface_plane): Cells must be neighbors to get interface plane"); let sign = if neighbors.is_negative() { -1.0 } else { 1.0 }; - let normal_dir = match axis { - 0 => [sign, 0.0, 0.0], - 1 => [0.0, sign, 0.0], - 2 => [0.0, 0.0, sign], - _ => unreachable!("Axis must be 0, 1, or 2"), - }; + let normal_dir = get_normal(axis, sign == -1.); let indices_cell = self.cell_points(cell1_id); @@ -432,13 +454,34 @@ impl CompartmentMeshManip for MeshCylindrical { let theta1 = self.get_cell_edge(1, indices_cell[1] + 1); let z0 = self.get_cell_edge(2, indices_cell[2]); let z1 = self.get_cell_edge(2, indices_cell[2] + 1); + let pi = std::f64::consts::PI; + let normalize = |a: f64| -> f64 { + let mut x = a % (2.0 * pi); + if x > pi { + x -= 2.0 * pi; + } + if x < -pi { + x += 2.0 * pi; + } + x + }; // Centers let r_center = 0.5 * (r0 + r1); - let theta_center = 0.5 * (theta0 + theta1); + let z_center = 0.5 * (z0 + z1); + // let theta_center = 0.5 * (theta0 + theta1); + let theta_center = { + let mut dtheta = theta1 - theta0; + if dtheta > pi { + dtheta -= 2.0 * pi; + } + if dtheta < -pi { + dtheta += 2.0 * pi; + } + normalize(theta0 + 0.5 * dtheta) + }; - // Origin of the plane (on interface) let (r, theta, z) = match axis { 0 => (if sign < 0.0 { r0 } else { r1 }, theta_center, z_center), 1 => (r_center, if sign < 0.0 { theta0 } else { theta1 }, z_center), @@ -447,18 +490,24 @@ impl CompartmentMeshManip for MeshCylindrical { }; let (extent_u, extent_v) = match axis { - 0 => ([theta0, theta1], [z0, z1]), // u = theta, v = z - 1 => ([r0, r1], [z0, z1]), // u = r, v = z - 2 => ([r0, r1], [theta0, theta1]), // u = r, v = theta + 0 => ([theta0, theta1], [z0, z1]), + 1 => ([r0, r1], [z0, z1]), + 2 => ([r0, r1], [theta0, theta1]), _ => unreachable!(), }; if axis == 0 { - // axe r -> plan tangent au cylindre - let bounded_plane = get_tangent_plane_at_r(axis, r, theta, z, extent_u, extent_v); + let normal_cartesian = CartesianVec3([theta.cos(), theta.sin(), 0.0]); + let origin = CartesianCoordinates([r * theta.cos(), r * theta.sin(), z]); + let bounded_plane = BoundedPlane { + normal: normal_cartesian, + origin, + extent_u, + extent_v, + axis, + }; (bounded_plane, axis) } else { - // pour axis 1 et 2 on garde ta méthode normale let cyl_normal = CylindricalVec3(normal_dir, theta); let normal_cartesian = cyl_normal.to_cartesian_vec(); let origin = CylindricalCoordinates([r, theta, z]).into(); @@ -473,6 +522,71 @@ impl CompartmentMeshManip for MeshCylindrical { } } + // fn get_interface_plane(&self, cell1_id: usize, cell2_id: usize) -> (BoundedPlane, usize) { + // let neighbors = self.are_cell_neighbor(cell1_id, cell2_id); + // let axis = neighbors + // .to_coord_index() + // .expect("Cells must be neighbors to get interface plane"); + + // let sign = if neighbors.is_negative() { -1.0 } else { 1.0 }; + + // let indices_cell = self.cell_points(cell1_id); + + // // Cell edges + // let r0 = self.get_cell_edge(0, indices_cell[0]); + // let r1 = self.get_cell_edge(0, indices_cell[0] + 1); + // let theta0 = self.get_cell_edge(1, indices_cell[1]); + // let theta1 = self.get_cell_edge(1, indices_cell[1] + 1); + // let z0 = self.get_cell_edge(2, indices_cell[2]); + // let z1 = self.get_cell_edge(2, indices_cell[2] + 1); + + // // Midpoints + // let r_center = 0.5 * (r0 + r1); + // let z_center = 0.5 * (z0 + z1); + // let theta_center = 0.5 * (theta0 + theta1); + + // // Extents + // let (extent_u, extent_v) = match axis { + // 0 => ([r0, r1], [z0, z1]), // Plane defined by (r, z) + // 1 => ([theta0, theta1], [z0, z1]), // Plane defined by (theta, z) + // 2 => ([r0, r1], [theta0, theta1]), // Plane defined by (r, theta) + // _ => unreachable!("Axis must be 0, 1, or 2"), + // }; + + // // Origin and normal + // let (origin, normal) = match axis { + // 0 => { + // // Plane defined by (r, z) + // let origin = CartesianCoordinates([r_center, theta_center, z_center]).into(); + // let normal = CartesianVec3([1.0, 0.0, 0.0]); // Radial direction + // (origin, normal) + // } + // 1 => { + // // Plane defined by (theta, z) + // let origin = CartesianCoordinates([r_center, theta_center, z_center]).into(); + // let normal = CartesianVec3([0.0, 1.0, 0.0]); // Angular direction + // (origin, normal) + // } + // 2 => { + // // Plane defined by (r, theta) + // let origin = CartesianCoordinates([r_center, theta_center, z_center]).into(); + // let normal = CartesianVec3([0.0, 0.0, 1.0]); // Axial direction + // (origin, normal) + // } + // _ => unreachable!("Axis must be 0, 1, or 2"), + // }; + + // let bounded_plane = BoundedPlane { + // normal, + // origin, + // extent_u, + // extent_v, + // axis, + // }; + + // (bounded_plane, axis) + // } + fn are_cell_neighbor(&self, cell1_id: usize, cell2_id: usize) -> NeighborDirection { let [r1, theta1, z1] = self.cell_points(cell1_id); let [r2, theta2, z2] = self.cell_points(cell2_id); @@ -539,10 +653,10 @@ impl CompartmentMeshManip for MeshCylindrical { CylindricalAxis::Theta => delta_ijk[0] * delta_ijk[2], // ds = dr*dz CylindricalAxis::Z => { // ds =r*dr*dtheta - // R here is not radius but (r-R) - let R = self.axes[0].edges[points_indices[0] + 1]; + // rr here is not radius but (r-R) + let rr = self.axes[0].edges[points_indices[0] + 1]; let r2 = self.axes[0].edges[points_indices[0]]; - 0.5 * (R * R - r2 * r2) * delta_ijk[1] + 0.5 * (rr * rr - r2 * r2) * delta_ijk[1] } } } @@ -562,6 +676,23 @@ impl CompartmentMeshManip for MeshCylindrical { delta_ijk[height_axis] * surface_ij } + fn cell_from_ax_points(&self, axis_points: &AxisPoints) -> Option { + let mut cell_1d = 0; + let mut multiplier = 1; + + for i in (0..self.axes.len()).rev() { + let n = self.axes[i].descriptor.n_range; + if axis_points[i] >= n { + return None; + } + cell_1d += axis_points[i] * multiplier; + + multiplier *= n; + } + + Some(cell_1d) + } + fn cell_from_coordinates(&self, coords: &Coords3) -> Option { let mut mesh_id = 0; let mut cumulative_product = 1; @@ -583,13 +714,12 @@ impl CompartmentMeshManip for MeshCylindrical { Some(mesh_id) } - fn is_point_inside(&self, cell_id: usize, point_coords: &Coords3) -> bool { + fn is_point_inside(&self, _cell_id: usize, _point_coords: &Coords3) -> bool { todo!() } fn cell_points(&self, cell_1d: usize) -> AxisPoints { let mut axis_points = AxisPoints::default(); - let mut p_coeff_up = cell_1d; // for (current_point, current_axis) in axis_points.iter_mut().zip(&self.axes) { @@ -608,6 +738,45 @@ impl CompartmentMeshManip for MeshCylindrical { axis_points } + + fn get_boundary(&self) -> Vec { + let (n_r, n_theta, n_z) = ( + self.n_points_axis(0), + self.n_points_axis(1), + self.n_points_axis(2), + ); + + let expected = n_theta * (n_z - 2) + 2 * n_r * n_z; + let mut v = Vec::with_capacity(expected); + + for i in 0..n_r { + for j in 0..n_theta { + let p = self + .cell_from_ax_points(&[i, j, 0]) + .expect("get_boundary: out of bound "); + let p2 = self + .cell_from_ax_points(&[i, j, n_z - 1]) + .expect("get_boundary: out of bound "); + v.push(p); + v.push(p2); + } + } + + for k in 1..n_z - 1 { + for j in 0..n_theta { + let p = self + .cell_from_ax_points(&[n_r - 1, j, k]) + .expect("get_boundary: out of bound "); + v.push(p) + } + } + + if expected != v.len() { + panic!("Detected number is not correct {} {}", expected, v.len()); + } + + v + } } pub fn get_mesh( @@ -628,259 +797,4 @@ pub fn get_mesh( } #[cfg(test)] -mod test { - - use super::*; - - const number_point_ax1: usize = 8; - const max_ax1: f64 = 4.; - - const number_point_ax2: usize = 5; - const max_ax2: f64 = 2.; - - const number_point_ax3: usize = 10; - const max_ax3: f64 = 10.; - - fn ref_mesh_cyclindrical() -> Box { - let ax1 = AxisDescriptor::new(0., max_ax1, number_point_ax1); - let ax2 = AxisDescriptor::new( - -std::f64::consts::PI, - std::f64::consts::PI, - number_point_ax2, - ); - let ax3 = AxisDescriptor::new(0., max_ax3, number_point_ax3); - get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]) - } - - #[test] - fn t_cell_surface_cylindrical() { - let ax1 = AxisDescriptor::new(0., 4., 10); - let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, 10); - let ax3 = AxisDescriptor::new(0., 2., 10); - - let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); - - let r_face_center = 0.4; - let dtheta = 2. * std::f64::consts::PI / 10.; - let dz = 2. / 10.; - let expected_surface = r_face_center * dtheta * dz; - let actual_surface = mesh.cell_surface(0, OrientedAxis::I); - assert!( - (actual_surface - expected_surface).abs() < 1e-10, - "Radial face surface incorrect: got {}, expected {}", - actual_surface, - expected_surface - ); - - let dr = 4. / 10.; // 0.4 - let expected_surface = dr * dz; - let actual_surface = mesh.cell_surface(0, OrientedAxis::J); - assert!( - (actual_surface - expected_surface).abs() < 1e-10, - "Theta face surface incorrect: got {}, expected {}", - actual_surface, - expected_surface - ); - - let r1 = 0.0; - let r2 = 0.4; - let dtheta = 2. * std::f64::consts::PI / 10.; - - let expected_surface = 0.5 * (r2 * r2 - r1 * r1) * dtheta; - let actual_surface = mesh.cell_surface(0, OrientedAxis::K); - assert!( - (actual_surface - expected_surface).abs() < 1e-10, - "Axial face surface incorrect: got {}, expected {}", - actual_surface, - expected_surface - ); - } - - #[test] - fn t_cell_volume_cylindrical() { - let ax1 = AxisDescriptor::new(0., 4., 10); - let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, 10); - let ax3 = AxisDescriptor::new(0., 2., 10); - let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); - - let r1 = 0.0; - let r2 = 0.4; - let dtheta = 2. * std::f64::consts::PI / 10.; - let dz = 2.0 / 10.0; - - let expected_volume = 0.5 * (r2 * r2 - r1 * r1) * dtheta * dz; - let actual_volume = mesh.cell_volume(0); - assert!( - (expected_volume - actual_volume).abs() < 1e-10, - "Axial face volume incorrect: got {}, expected {}", - actual_volume, - expected_volume - ); - - let expected_full_volume = std::f64::consts::PI * 4. * 4. * 2.; - let full_volume: f64 = (0..mesh.number_cell()).map(|e| mesh.cell_volume(e)).sum(); - assert!( - (expected_full_volume - full_volume).abs() < 1e-10, - "full_volume incorrect: got {}, expected {}", - full_volume, - expected_full_volume - ); - } - - #[test] - fn t_get_mesh() { - let ax1 = AxisDescriptor::new(0.5, 1., 10); - let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, 20); - let ax3 = AxisDescriptor::new(0., 2., 15); - let expected_step_x = 1. / 10.; //Cylindrical start ax from 0 to max_range - let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); - assert!( - mesh.mesh_step_axis(0) == expected_step_x, - "{} {}", - mesh.mesh_step_axis(0), - expected_step_x - ); - } - - #[test] - fn t_getter() { - let mesh = ref_mesh_cyclindrical(); - assert!(mesh.max_axis(0) == max_ax1); - assert!(mesh.max_axis(1) == std::f64::consts::PI); - assert!(mesh.max_axis(2) == max_ax3); - - assert!(mesh.min_axis(0) == 0.); - assert!(mesh.min_axis(1) == -std::f64::consts::PI); - - assert!(mesh.n_points_axis(0) == number_point_ax1); - assert!(mesh.n_points_axis(1) == number_point_ax2); - assert!(mesh.n_points_axis(2) == number_point_ax3); - assert!(mesh.number_cell() == number_point_ax1 * number_point_ax2 * number_point_ax3); - } - - #[test] - fn t_identification_cylindrical() { - let mesh = ref_mesh_cyclindrical(); - - let assert_id = |a: Coords3, expect: usize| { - let CartesianCoordinates(aa) = CylindricalCoordinates(a).into(); - - let id1 = mesh - .cell_from_coordinates(&aa) - .expect("Test neighbors: coordinates for cell a are outside the mesh."); - - assert!( - id1 == expect, - "Assertion failed: expected {:?}, got {:?}", - expect, - id1 - ); - }; - - assert_id([0., -std::f64::consts::PI, 0.], 0); - - assert_id([0., -std::f64::consts::PI, max_ax3], number_point_ax3 - 1); - let theta = -std::f64::consts::PI + mesh.mesh_step_axis(1) * 1.1; - //R!=0 because with cartesian conversion is x=rcos(theta) if theta changes but no r its the same compartment - assert_id([0.01, theta, 0.], number_point_ax3); - //-1 because we consider cell ID for 0 to n-1 - assert_id( - [max_ax1, std::f64::consts::PI, max_ax3], - (number_point_ax3 * number_point_ax1 * number_point_ax2) - 1, - ); - } - - #[test] - fn t_neighbors_cylindrical() { - use NeighborDirection::*; - - let mesh = ref_mesh_cyclindrical(); - - let assert_neighbors = - |a: CylindricalCoordinates, b: CylindricalCoordinates, expected: NeighborDirection| { - let CartesianCoordinates(aa) = a.into(); - - let CartesianCoordinates(bb) = b.into(); - - let id1 = mesh - .cell_from_coordinates(&aa) - .expect("Test neighbors: coordinates for cell a are outside the mesh."); - let id2 = mesh - .cell_from_coordinates(&bb) - .expect("Test neighbors: coordinates for cell b are outside the mesh."); - - let neighbors = mesh.are_cell_neighbor(id1, id2); - assert!( - neighbors == expected, - "Assertion failed: expected {:?}, got {:?}", - expected, - neighbors - ); - }; - - // Fixed coordinates and offsets for testing - let fix_i = 2.2; - let offset_i = mesh.mesh_step_axis(0); - let fix_j = 0.; - let offset_j = 0.68; - let fix_k = 4.0; - let offset_k = 0.9; - - // Assert neighbor relationships in different directions - - // Testing in X direction - assert_neighbors( - CylindricalCoordinates([fix_i, fix_j, fix_k]), - CylindricalCoordinates([fix_i + offset_i, fix_j, fix_k]), - XPlus, - ); - assert_neighbors( - CylindricalCoordinates([fix_i + offset_i, fix_j, fix_k]), - CylindricalCoordinates([fix_i, fix_j, fix_k]), - XMinus, - ); - - // Testing in Y direction - assert_neighbors( - CylindricalCoordinates([fix_i, fix_j + offset_j, fix_k]), - CylindricalCoordinates([fix_i, fix_j, fix_k]), - YMinus, - ); - assert_neighbors( - CylindricalCoordinates([fix_i, fix_j, fix_k]), - CylindricalCoordinates([fix_i, fix_j + offset_j, fix_k]), - YPlus, - ); - - // Testing in Z direction - assert_neighbors( - CylindricalCoordinates([fix_i, fix_j, fix_k + offset_k]), - CylindricalCoordinates([fix_i, fix_j, fix_k]), - ZMinus, - ); - assert_neighbors( - CylindricalCoordinates([fix_i, fix_j, fix_k]), - CylindricalCoordinates([fix_i, fix_j, fix_k + offset_k]), - ZPlus, - ); - - // Testing non-neighbor cases - assert_neighbors( - CylindricalCoordinates([0., fix_j, fix_k + offset_k]), - CylindricalCoordinates([fix_i, fix_j, fix_k]), - NotNeighbors, - ); - assert_neighbors( - CylindricalCoordinates([fix_i, fix_j, fix_k]), - CylindricalCoordinates([0., fix_j, fix_k + offset_k]), - NotNeighbors, - ); - let little_offset = mesh.mesh_step_axis(0) * 1.005; - let c1: f64 = mesh.get_cell_edge(0, 2); - assert_neighbors( - CylindricalCoordinates([c1, fix_j, fix_k]), - CylindricalCoordinates([c1 + little_offset, fix_j, fix_k]), - NotNeighbors, - ); - } -} +mod tests; diff --git a/cmtool-core/src/grid/tests.rs b/cmtool-core/src/grid/tests.rs new file mode 100644 index 00000000..23472ff8 --- /dev/null +++ b/cmtool-core/src/grid/tests.rs @@ -0,0 +1,440 @@ +use super::*; +#[cfg(test)] +mod test { + + use super::*; + + const NUMBER_POINT_AX1: usize = 8; + const MAX_AX1: f64 = 4.; + + const NUMBER_POINT_AX2: usize = 5; + const _MAX_AX2: f64 = 2.; + + const NUMBER_POINT_AX3: usize = 10; + const MAX_AX3: f64 = 10.; + + fn utils_planes_neighbors( + nax1: usize, + nax2: usize, + nax3: usize, + ) -> (Box, f64, f64, f64) { + let ax1 = AxisDescriptor::new(0., MAX_AX1, nax1); + let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, nax2); + + let ax3 = AxisDescriptor::new(0., MAX_AX3, nax3); + + let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); + + let step_r = MAX_AX1 / (nax1 as f64); + let step_theta = 2.0 * std::f64::consts::PI / (nax2 as f64); + let step_z = MAX_AX3 / (nax3 as f64); + + (mesh, step_r, step_theta, step_z) + } + + fn ref_mesh_cyclindrical() -> Box { + let ax1 = AxisDescriptor::new(0., MAX_AX1, NUMBER_POINT_AX1); + let ax2 = AxisDescriptor::new( + -std::f64::consts::PI, + std::f64::consts::PI, + NUMBER_POINT_AX2, + ); + let ax3 = AxisDescriptor::new(0., MAX_AX3, NUMBER_POINT_AX3); + get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]) + } + + #[test] + fn t_get_mesh() { + let ax1 = AxisDescriptor::new(0.5, 1., 10); + let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, 20); + let ax3 = AxisDescriptor::new(0., 2., 15); + let expected_step_x = 1. / 10.; //Cylindrical start ax from 0 to max_range + let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); + assert!( + mesh.mesh_step_axis(0) == expected_step_x, + "{} {}", + mesh.mesh_step_axis(0), + expected_step_x + ); + } + + #[test] + fn t_getter() { + let mesh = ref_mesh_cyclindrical(); + assert!(mesh.max_axis(0) == MAX_AX1); + assert!(mesh.max_axis(1) == std::f64::consts::PI); + assert!(mesh.max_axis(2) == MAX_AX3); + + assert!(mesh.min_axis(0) == 0.); + assert!(mesh.min_axis(1) == -std::f64::consts::PI); + + assert!(mesh.n_points_axis(0) == NUMBER_POINT_AX1); + assert!(mesh.n_points_axis(1) == NUMBER_POINT_AX2); + assert!(mesh.n_points_axis(2) == NUMBER_POINT_AX3); + assert!(mesh.number_cell() == NUMBER_POINT_AX1 * NUMBER_POINT_AX2 * NUMBER_POINT_AX3); + } + + #[test] + fn t_cell_surface_cylindrical() { + let ax1 = AxisDescriptor::new(0., 4., 10); + let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, 10); + let ax3 = AxisDescriptor::new(0., 2., 10); + + let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); + + let r_face_center = 0.4; + let dtheta = 2. * std::f64::consts::PI / 10.; + let dz = 2. / 10.; + let expected_surface = r_face_center * dtheta * dz; + let actual_surface = mesh.cell_surface(0, OrientedAxis::I); + assert!( + (actual_surface - expected_surface).abs() < 1e-10, + "Radial face surface incorrect: got {}, expected {}", + actual_surface, + expected_surface + ); + + let dr = 4. / 10.; // 0.4 + let expected_surface = dr * dz; + let actual_surface = mesh.cell_surface(0, OrientedAxis::J); + assert!( + (actual_surface - expected_surface).abs() < 1e-10, + "Theta face surface incorrect: got {}, expected {}", + actual_surface, + expected_surface + ); + + let r1 = 0.0; + let r2 = 0.4; + let dtheta = 2. * std::f64::consts::PI / 10.; + + let expected_surface = 0.5 * (r2 * r2 - r1 * r1) * dtheta; + let actual_surface = mesh.cell_surface(0, OrientedAxis::K); + assert!( + (actual_surface - expected_surface).abs() < 1e-10, + "Axial face surface incorrect: got {}, expected {}", + actual_surface, + expected_surface + ); + } + + #[test] + fn t_cell_volume_cylindrical() { + let ax1 = AxisDescriptor::new(0., 4., 10); + let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, 10); + let ax3 = AxisDescriptor::new(0., 2., 10); + let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); + + let r1 = 0.0; + let r2 = 0.4; + let dtheta = 2. * std::f64::consts::PI / 10.; + let dz = 2.0 / 10.0; + + let expected_volume = 0.5 * (r2 * r2 - r1 * r1) * dtheta * dz; + let actual_volume = mesh.cell_volume(0); + assert!( + (expected_volume - actual_volume).abs() < 1e-10, + "Axial face volume incorrect: got {}, expected {}", + actual_volume, + expected_volume + ); + + let expected_full_volume = std::f64::consts::PI * 4. * 4. * 2.; + let full_volume: f64 = (0..mesh.number_cell()).map(|e| mesh.cell_volume(e)).sum(); + assert!( + (expected_full_volume - full_volume).abs() < 1e-10, + "full_volume incorrect: got {}, expected {}", + full_volume, + expected_full_volume + ); + } + + #[test] + fn t_identification_cylindrical() { + let mesh = ref_mesh_cyclindrical(); + + let assert_id = |a: Coords3, expect: usize| { + let CartesianCoordinates(aa) = CylindricalCoordinates(a).into(); + + let id1 = mesh + .cell_from_coordinates(&aa) + .expect("Test neighbors: coordinates for cell a are outside the mesh."); + + assert!( + id1 == expect, + "Assertion failed: expected {:?}, got {:?}", + expect, + id1 + ); + }; + + assert_id([0., -std::f64::consts::PI, 0.], 0); + + assert_id([0., -std::f64::consts::PI, MAX_AX3], NUMBER_POINT_AX3 - 1); + let theta = -std::f64::consts::PI + mesh.mesh_step_axis(1) * 1.1; + //R!=0 because with cartesian conversion is x=rcos(theta) if theta changes but no r its the same compartment + assert_id([0.01, theta, 0.], NUMBER_POINT_AX3); + //-1 because we consider cell ID for 0 to n-1 + assert_id( + [MAX_AX1, std::f64::consts::PI, MAX_AX3], + (NUMBER_POINT_AX3 * NUMBER_POINT_AX1 * NUMBER_POINT_AX2) - 1, + ); + } + + #[test] + fn t_boundary_cylindrical() { + let ax1 = AxisDescriptor::new(0., MAX_AX1, 5); + let ax2 = AxisDescriptor::new(-std::f64::consts::PI, std::f64::consts::PI, 10); + let ax3 = AxisDescriptor::new(0., MAX_AX3, 10); + let mesh = get_mesh(MeshType::Cylindrical, [ax1, ax2, ax3]); + + let mut v = mesh.get_boundary(); + let w = [ + 0, 9, 10, 19, 20, 29, 30, 39, 40, 49, 50, 59, 60, 69, 70, 79, 80, 89, 90, 99, 100, 109, + 110, 119, 120, 129, 130, 139, 140, 149, 150, 159, 160, 169, 170, 179, 180, 189, 190, + 199, 200, 209, 210, 219, 220, 229, 230, 239, 240, 249, 250, 259, 260, 269, 270, 279, + 280, 289, 290, 299, 300, 309, 310, 319, 320, 329, 330, 339, 340, 349, 350, 359, 360, + 369, 370, 379, 380, 389, 390, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, + 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, + 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, + 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, + 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, + 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, + 495, 496, 497, 498, 499, + ]; + v.sort(); + assert_eq!(v, w); + } + + #[test] + fn test_theta_plane_neighbors() { + let (mesh, step_r, step_theta, step_z) = + utils_planes_neighbors(NUMBER_POINT_AX1, NUMBER_POINT_AX2, NUMBER_POINT_AX3); + let eps_step = 0.1; + let aa = [0.1, -std::f64::consts::PI, 0.0]; + let bb = [0.1, -std::f64::consts::PI + step_theta + eps_step, 0.0]; + let CartesianCoordinates(aa) = CylindricalCoordinates(aa).into(); + let CartesianCoordinates(bb) = CylindricalCoordinates(bb).into(); + let cell1_id = mesh.cell_from_coordinates(&aa).unwrap(); + let cell2_id = mesh.cell_from_coordinates(&bb).unwrap(); + + assert!(cell1_id != cell2_id); + + let (plane, axis) = mesh.get_interface_plane(cell1_id, cell2_id); + assert_eq!(axis, 1); + assert!((plane.extent_u[0] - 0.0).abs() < 1e-8); // r min + assert!((plane.extent_u[1] - step_r).abs() < 1e-8); // r max + assert!((plane.extent_v[0] - 0.0).abs() < 1e-8); // z min + assert!((plane.extent_v[1] - step_z).abs() < 1e-8); // z max + // assert!(plane.origin[0]==mesh.ge + } + + #[test] + fn test_r_plane_neighbors() { + let (mesh, step_r, step_theta, step_z) = + utils_planes_neighbors(NUMBER_POINT_AX1, NUMBER_POINT_AX2, NUMBER_POINT_AX3); + let eps_step = 0.1; + let aa = [0.0, 0.0, 0.0]; + let bb = [step_r + eps_step, 0.0, 0.0]; + + let CartesianCoordinates(aa) = CylindricalCoordinates(aa).into(); + let CartesianCoordinates(bb) = CylindricalCoordinates(bb).into(); + + let cell1_id = mesh.cell_from_coordinates(&aa).unwrap(); + let cell2_id = mesh.cell_from_coordinates(&bb).unwrap(); + + assert!(cell1_id != cell2_id); + + let m = mesh.cell_points(cell1_id); + let mp = mesh.get_cell_edge(1, m[1]); + let mn = mesh.get_cell_edge(1, m[1] + 1); + + let (plane, axis) = mesh.get_interface_plane(cell1_id, cell2_id); + assert_eq!(axis, 0); + assert!( + (plane.extent_u[0] - mp).abs() < 1e-8, + "{} vs {}", + plane.extent_u[0], + step_theta + ); + assert!( + (plane.extent_u[1] - mn).abs() < 1e-8, + "{} vs {}", + plane.extent_u[1], + 0. + ); + + assert!((plane.extent_v[0] - 0.0).abs() < 1e-8); + assert!((plane.extent_v[1] - step_z).abs() < 1e-8); + } + + #[test] + fn fuzz_z_plane_neighbors() { + let nax1 = [5, 7, 10]; + let nax2 = [5, 7, 10]; + let nax3 = [5, 7, 10]; + + for &n1 in nax1.iter() { + for &n2 in nax2.iter() { + for &n3 in nax3.iter() { + eprintln!("{} {} {}", n1, n2, n3); + test_z_plane_neighbors(n1, n2, n3); + } + } + } + } + + fn test_z_plane_neighbors(nax1: usize, nax2: usize, nax3: usize) { + let (mesh, step_r, step_theta, step_z) = utils_planes_neighbors(nax1, nax2, nax3); + let eps_step = 0.1; + let aa = [0.0, 0.0, 0.0]; + let bb = [0., 0.0, step_z + eps_step]; + + let CartesianCoordinates(aa) = CylindricalCoordinates(aa).into(); + let CartesianCoordinates(bb) = CylindricalCoordinates(bb).into(); + + let cell1_id = mesh.cell_from_coordinates(&aa).unwrap(); + let cell2_id = mesh.cell_from_coordinates(&bb).unwrap(); + + assert!(cell1_id != cell2_id); + + let (plane, axis) = mesh.get_interface_plane(cell1_id, cell2_id); + assert_eq!(axis, 2); + let m = mesh.cell_points(cell1_id); + let mp = mesh.get_cell_edge(1, m[1]); + let mn = mesh.get_cell_edge(1, m[1] + 1); + let theta = aa[1]; + let theta_ext = theta - step_theta / 2.; + let theta_in = theta + step_theta / 2.; + + // let mut origin: Vec = (0..3).map(|i| mesh.get_cell_center(i, m[i])).collect(); + // origin[2] = step_z; + + // assert!( + // origin == plane.origin.0, + // "{:?} {:?}", + // origin, + // plane.origin.0 + // ); + + assert!( + (plane.extent_v[0] - mp).abs() < 1e-8, + "1 {} expected {}", + plane.extent_v[0], + theta_ext + ); + assert!( + (plane.extent_v[1] - mn).abs() < 1e-8, + "2 {} expected {}", + theta_in, + 0. + ); + + assert!( + (plane.extent_u[0] - 0.0).abs() < 1e-8, + "3 {} expected {}", + plane.extent_u[0], + 0. + ); + assert!( + (plane.extent_u[1] - step_r).abs() < 1e-8, + "4 {} expected {}", + plane.extent_u[1], + step_z + ); + } + + #[test] + fn t_neighbors_cylindrical() { + use NeighborDirection::*; + + let mesh = ref_mesh_cyclindrical(); + + let assert_neighbors = + |a: CylindricalCoordinates, b: CylindricalCoordinates, expected: NeighborDirection| { + let CartesianCoordinates(aa) = a.into(); + + let CartesianCoordinates(bb) = b.into(); + + let id1 = mesh + .cell_from_coordinates(&aa) + .expect("Test neighbors: coordinates for cell a are outside the mesh."); + let id2 = mesh + .cell_from_coordinates(&bb) + .expect("Test neighbors: coordinates for cell b are outside the mesh."); + + let neighbors = mesh.are_cell_neighbor(id1, id2); + assert!( + neighbors == expected, + "Assertion failed: expected {:?}, got {:?}", + expected, + neighbors + ); + }; + + // Fixed coordinates and offsets for testing + let fix_i = 2.2; + let offset_i = mesh.mesh_step_axis(0); + let fix_j = 0.; + let offset_j = 0.68; + let fix_k = 4.0; + let offset_k = 0.9; + + // Assert neighbor relationships in different directions + + // Testing in X direction + assert_neighbors( + CylindricalCoordinates([fix_i, fix_j, fix_k]), + CylindricalCoordinates([fix_i + offset_i, fix_j, fix_k]), + XPlus, + ); + assert_neighbors( + CylindricalCoordinates([fix_i + offset_i, fix_j, fix_k]), + CylindricalCoordinates([fix_i, fix_j, fix_k]), + XMinus, + ); + + // Testing in Y direction + assert_neighbors( + CylindricalCoordinates([fix_i, fix_j + offset_j, fix_k]), + CylindricalCoordinates([fix_i, fix_j, fix_k]), + YMinus, + ); + assert_neighbors( + CylindricalCoordinates([fix_i, fix_j, fix_k]), + CylindricalCoordinates([fix_i, fix_j + offset_j, fix_k]), + YPlus, + ); + + // Testing in Z direction + assert_neighbors( + CylindricalCoordinates([fix_i, fix_j, fix_k + offset_k]), + CylindricalCoordinates([fix_i, fix_j, fix_k]), + ZMinus, + ); + assert_neighbors( + CylindricalCoordinates([fix_i, fix_j, fix_k]), + CylindricalCoordinates([fix_i, fix_j, fix_k + offset_k]), + ZPlus, + ); + + // Testing non-neighbor cases + assert_neighbors( + CylindricalCoordinates([0., fix_j, fix_k + offset_k]), + CylindricalCoordinates([fix_i, fix_j, fix_k]), + NotNeighbors, + ); + assert_neighbors( + CylindricalCoordinates([fix_i, fix_j, fix_k]), + CylindricalCoordinates([0., fix_j, fix_k + offset_k]), + NotNeighbors, + ); + let little_offset = mesh.mesh_step_axis(0) * 1.005; + let c1: f64 = mesh.get_cell_edge(0, 2); + assert_neighbors( + CylindricalCoordinates([c1, fix_j, fix_k]), + CylindricalCoordinates([c1 + little_offset, fix_j, fix_k]), + NotNeighbors, + ); + } +} diff --git a/cmtool-core/src/lib.rs b/cmtool-core/src/lib.rs index 12be5fb3..a9049f80 100644 --- a/cmtool-core/src/lib.rs +++ b/cmtool-core/src/lib.rs @@ -1,50 +1,53 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use std::{path::Path, sync::Arc}; - -use cmtool_data::{RawData, RawDataFlux, RawDataScalar}; - -use crate::model::{CMGeometry, CMModel, Scalar, Vector}; - -#[cfg(feature = "use_vtk")] -use crate::grid::vtk::VtkCm; -#[cfg(feature = "use_vtk")] -use crate::grid::vtk::add_celldata_to_vtk; - pub mod coordinates; pub mod ensight_gold; +mod errors; pub mod grid; pub mod model; pub mod utils; +pub use errors::CoreError; + +#[cfg(feature = "use_vtk")] +use grid::vtk::VtkCm; +#[cfg(feature = "use_vtk")] +use grid::vtk::add_celldata_to_vtk; + +use cmtool_data::{RawData, RawDataFlux, RawDataScalar}; +use model::{CMGeometry, CMModel, Scalar, Vector}; +use std::cmp::Ordering; +use std::{path::Path, sync::Arc}; + +fn resolve_path( + root: &impl AsRef, + relative_path: &str, +) -> impl AsRef { + std::path::PathBuf::from(root.as_ref()).join(relative_path) +} + pub enum ExportType { EnsightGold, } -trait GeometryInfo {} - -trait CfdCase { - fn get_root(&self) -> String; - fn get_geometry_relative_path(&self) -> String; -} +// trait GeometryInfo {} -mod errors; -pub use errors::CoreError; +// trait CfdCase { +// fn get_root(&self) -> String; +// fn get_geometry_relative_path(&self) -> String; +// } pub struct CMHandle { model: Arc, - root_result: String, + _root_result: String, //TODO EITHER USE IT OR REMOVE eg_geometry: Arc, cm_geometry: Arc, } -fn resolve_path( - root: &impl AsRef, - relative_path: &str, -) -> impl AsRef { - std::path::PathBuf::from(root.as_ref()).join(relative_path) -} - impl CMHandle { + pub fn grid(&self) -> &dyn crate::grid::CompartmentMesh { + self.model.grid() + } + pub fn init( n_div: [usize; 3], root: &str, @@ -71,12 +74,9 @@ impl CMHandle { grid::MeshType::Cylindrical, )); - // let fullpath = format!("{}/wall_cart.scl1", root); - // let s = ensight_gold::scalar::ScalarField::init(eg_geometry, Path::new(&fullpath.clone()))?; - Ok(Self { model: Arc::new(CMModel::init(cm_geometry.clone())), - root_result: String::from("./test"), + _root_result: String::from("./test"), eg_geometry, cm_geometry, }) @@ -88,30 +88,6 @@ impl CMHandle { todo!() } - #[cfg(feature = "use_vtk")] - pub fn write_vtk(&self, path: impl AsRef) -> Result<(), CoreError> { - let mesh = self.cm_geometry.get_grid().unwrap(); - let p = path.as_ref().to_str().unwrap(); - let mut vtk = mesh.get_vtk(p)?; - - let volumes_data = self.model.get_real_volume(); - - let volumes_data_array = vtkio::model::DataArray::scalars("real_volume", 1); - - let volumes_data_array = volumes_data_array.with_vec(volumes_data); - - add_celldata_to_vtk( - &mut vtk, - vtkio::model::Attribute::DataArray(volumes_data_array), - ); - - let mut vtk_bytes = Vec::::new(); - vtk.write_xml(&mut vtk_bytes).unwrap(); - std::fs::write(path, vtk_bytes).unwrap(); - - Ok(()) - } - pub fn dump_all( &self, root_export: impl AsRef, @@ -120,42 +96,29 @@ impl CMHandle { ) -> Result<(), CoreError> { std::fs::create_dir_all(&root_export)?; - // let mut handles = Vec::new(); + let mut rs = Vec::with_capacity(vars.len()); + let mut v: Vec = vars.to_owned(); + + v.sort_by(|a, _b| { + if a.get_type() == ensight_gold::case::VariableType::Scalar { + Ordering::Less + } + // } else if b.get_type() == ensight_gold::case::VariableType::Scalar { + // Ordering::Less + // } + else { + Ordering::Equal + } + }); + for v in vars.iter() { - // let res_name = resolve_path(&root_export, &v.name) - // .as_ref() - // .to_str() - // .unwrap() - // .to_owned(); - - // let path = resolve_path(&root_input, &v.filepath) - // .as_ref() - // .to_str() - // .unwrap() - // .to_owned(); - // let eg_geometry_clone = self.eg_geometry.clone(); - // let cm_geometry_clone = self.cm_geometry.clone(); - // let model_clone = self.model.clone(); match v.get_type() { ensight_gold::case::VariableType::Scalar => { - self.dump_scalar( + let sc = self.dump_scalar( resolve_path(&root_export, &v.name), resolve_path(&root_input, &v.filepath), )?; - - // let eg_geometry_clone = self.eg_geometry.clone(); - // let cm_geometry_clone = self.cm_geometry.clone(); - // let model_clone = self.model.clone(); - - // handles.push(std::thread::spawn(move || { - // Self::ts_dump_scalar( - // res_name, - // path, - // eg_geometry_clone, - // cm_geometry_clone, - // model_clone, - // ) - // })); + rs.push((sc, v.name.clone())); } ensight_gold::case::VariableType::Vector => { self.dump_vector( @@ -166,41 +129,49 @@ impl CMHandle { } } - // for i in handles - // { - // match i.join() { - // Ok(Ok(_)) => println!("Thread executed successfully"), - // Ok(Err(e)) => println!("Thread failed with error: {:?}", e), - // Err(e) => println!("Thread panicked: {:?}", e), - // } - // } + #[cfg(feature = "use_vtk")] + self.export_vtk( + format!("{}/cma_case.vtu", root_export.as_ref().display()), + rs, + )?; Ok(()) } + pub fn get_scalar(&self, path: impl AsRef) -> Result { + Self::s_get_scalar(path, self.eg_geometry.clone(), self.cm_geometry.clone()) + } + + fn s_get_scalar( + path: impl AsRef, + eg_geometry: Arc, + cm_geometry: Arc, + ) -> Result { + let s = ensight_gold::scalar::ScalarField::init(eg_geometry.clone(), path)?; + Ok(Scalar::new(s, &cm_geometry, &eg_geometry)) + } + fn ts_dump_scalar( res_name: impl AsRef, path: impl AsRef, eg_geometry: Arc, cm_geometry: Arc, model: Arc, - ) -> Result<(), CoreError> { - let s = ensight_gold::scalar::ScalarField::init(eg_geometry.clone(), path)?; - - let scalar = Scalar::new(s, &cm_geometry, &eg_geometry); + ) -> Result { + let scalar = Self::s_get_scalar(path, eg_geometry, cm_geometry)?; let scalar_data = model.export_volume_integral_per_zone(scalar)?; scalar_data.write_raw(&format!("{}.raw", res_name.as_ref().to_str().unwrap()))?; - Ok(()) + Ok(scalar_data) } pub fn dump_scalar( &self, res_name: impl AsRef, path: impl AsRef, - ) -> Result<(), CoreError> { + ) -> Result { Self::ts_dump_scalar( res_name, path, @@ -218,6 +189,33 @@ impl CMHandle { Ok(()) } + pub fn dump_vector_phase_fraction( + &self, + res_name: impl AsRef, + path: impl AsRef, + phase_fraction: Scalar, + ) -> Result<(), CoreError> { + let v = ensight_gold::vectors::VectorField::init(self.eg_geometry.clone(), path)?; + let vector = + Vector::new(v, &self.cm_geometry, &self.eg_geometry).scale_by(phase_fraction)?; + + let flow_data = self.model.compute_flux_between_compartments(vector)?; + + flow_data.write_raw(&format!("{}.raw", res_name.as_ref().to_str().unwrap()))?; + Ok(()) + } + + pub fn dump_vector_raw( + &self, + res_name: impl AsRef, + vector: Vector, + ) -> Result<(), CoreError> { + let flow_data = self.model.compute_flux_between_compartments(vector)?; + + flow_data.write_raw(&format!("{}.raw", res_name.as_ref().to_str().unwrap()))?; + Ok(()) + } + pub fn dump_vector( &self, res_name: impl AsRef, @@ -231,17 +229,26 @@ impl CMHandle { Ok(()) } - pub fn dump_vector_from_scalar( + pub fn vector_from_scalar( &self, - res_name: impl AsRef, path_i: impl AsRef, path_j: impl AsRef, path_k: impl AsRef, - ) -> Result { + ) -> Result { let s = ensight_gold::scalar::ScalarField::init(self.eg_geometry.clone(), path_i)?; let sj = ensight_gold::scalar::ScalarField::init(self.eg_geometry.clone(), path_j)?; let sk = ensight_gold::scalar::ScalarField::init(self.eg_geometry.clone(), path_k)?; - let vector = Vector::from_scalar([s, sj, sk], &self.cm_geometry, &self.eg_geometry)?; + Vector::from_scalar([s, sj, sk], &self.cm_geometry, &self.eg_geometry) + } + + pub fn dump_vector_from_scalar( + &self, + res_name: impl AsRef, + path_i: impl AsRef, + path_j: impl AsRef, + path_k: impl AsRef, + ) -> Result { + let vector = self.vector_from_scalar(path_i, path_j, path_k)?; let flow_data = self.model.compute_flux_between_compartments(vector)?; flow_data.write_raw(&format!("{}.raw", res_name.as_ref().to_str().unwrap()))?; @@ -251,4 +258,59 @@ impl CMHandle { pub fn export_geometry_compartments(&self) { todo!() } + + // #[cfg(feature = "use_vtk")] + // pub fn write_vtk(&self, path: impl AsRef) -> Result<(), CoreError> { + // let mesh = self.cm_geometry.get_grid().unwrap(); + // let p = path.as_ref().to_str().unwrap(); + // let mut vtk = mesh.get_vtk(p)?; + + // let volumes_data = self.model.get_real_volume(); + + // let volumes_data_array = vtkio::model::DataArray::scalars("real_volume", 1); + + // let volumes_data_array = volumes_data_array.with_vec(volumes_data); + + // add_celldata_to_vtk( + // &mut vtk, + // vtkio::model::Attribute::DataArray(volumes_data_array), + // ); + + // let mut vtk_bytes = Vec::::new(); + // vtk.write_xml(&mut vtk_bytes).unwrap(); + // std::fs::write(path, vtk_bytes).unwrap(); + + // Ok(()) + // } + + #[cfg(feature = "use_vtk")] + fn export_vtk( + &self, + path: impl AsRef, + sc: Vec<(RawDataScalar, String)>, + ) -> Result<(), CoreError> { + let mesh = self.cm_geometry.get_grid().unwrap(); + let p = path.as_ref().to_str().unwrap(); + let mut vtk = mesh.get_vtk(p)?; + + let mut add_cell = |name: &str, data: Vec| { + let data_array = vtkio::model::DataArray::scalars(name, 1); + let data_array = data_array.with_vec(data); + add_celldata_to_vtk(&mut vtk, vtkio::model::Attribute::DataArray(data_array)); + }; + + sc.iter().for_each(|(r, n)| { + let ve: Vec = r.values.iter().map(|i| i.value).collect(); + add_cell(n, ve) + }); + + let volumes_data = self.model.get_real_volume(); + add_cell("real_volume", volumes_data); + + let mut vtk_bytes = Vec::::new(); + vtk.write_xml(&mut vtk_bytes).unwrap(); + std::fs::write(path, vtk_bytes).unwrap(); + + Ok(()) + } } diff --git a/cmtool-core/src/model/compartments.rs b/cmtool-core/src/model/compartments.rs index 7ee88bc4..8a9b9c5b 100644 --- a/cmtool-core/src/model/compartments.rs +++ b/cmtool-core/src/model/compartments.rs @@ -44,10 +44,9 @@ impl CompartmentInfo { let mut tmp_count_k_element: Vec = vec![0; geometry.n_zone()]; - for (volume_element_global_id, n_compartment_in_velem) in + for (volume_element_global_id, &n_compartment_in_velem) in geometry.volume_elements.enumerate_number_id() { - let n_compartment_in_velem = *n_compartment_in_velem; let (elem_type, n_vertex) = geometry .volume_elements .get_element_and_nvertex(volume_element_global_id); @@ -66,7 +65,7 @@ impl CompartmentInfo { let volume = compute_volume(&local_vertices, elem_type).unwrap() / (n_compartment_in_velem as f64); - assert!(volume >= 0.); + assert!(volume >= 0., "CMTOOL(\"fill\"): Negative volume"); self.volumes[compartment_id][k_element] = ElementVolumeInfo { global_id: volume_element_global_id, volume, diff --git a/cmtool-core/src/model/geometry.rs b/cmtool-core/src/model/geometry.rs index 2fb76f3a..c0aa69cf 100644 --- a/cmtool-core/src/model/geometry.rs +++ b/cmtool-core/src/model/geometry.rs @@ -200,18 +200,21 @@ impl CMGeometry { let mut count = CountVolumeElement::new(self.n_zone()); let grid = self.grid.as_ref().unwrap(); - for (_gid, interface_cid_0, interface_cid_k, k_vertex) in self.interface_iter() { - count.incr_compartment(interface_cid_k); - - if k_vertex >= 1 { - let neighbors = grid.are_cell_neighbor(interface_cid_0, interface_cid_k); + for (vol_element_global_id, _, interface_cid_k, k_vertex) in self.interface_iter() { + count.incr_compartment(interface_cid_k); + // if k_vertex >= 1 { + for i in 0..k_vertex { + let cid_i = self + .volume_elements + .get_list_compartment_id(vol_element_global_id, i); + let neighbors = grid.are_cell_neighbor(cid_i, interface_cid_k); if neighbors != NeighborDirection::NotNeighbors { - let (id1, id2) = neighbors.ordered_pair(interface_cid_0, interface_cid_k); - + let (id1, id2) = neighbors.ordered_pair(cid_i, interface_cid_k); count.incr_interface(id1, id2); } } + // } } count diff --git a/cmtool-core/src/model/interfaces.rs b/cmtool-core/src/model/interfaces.rs index 766eb791..949d26b2 100644 --- a/cmtool-core/src/model/interfaces.rs +++ b/cmtool-core/src/model/interfaces.rs @@ -11,7 +11,7 @@ pub struct InterfaceInfo { pub target_id: usize, } -#[derive(Clone)] +#[derive(Clone, Copy, Debug)] pub struct InterfaceFlow { pub source_flow: f64, pub target_flow: f64, @@ -23,6 +23,7 @@ pub struct AInterfacesInfo { pub area: Vec>, pub normal_axis: Vec, pub global_id_from_interface: Vec>, + pub interface_theta: Vec, // pub plane_coordinates: Vec, // pub planes: Vec, } @@ -37,6 +38,7 @@ impl AInterfacesInfo { area: vec![Default::default(); n_interfaces], normal_axis: vec![Default::default(); n_interfaces], global_id_from_interface: vec![Default::default(); n_interfaces], + interface_theta: vec![Default::default(); n_interfaces], // plane_coordinates: vec![0.; n_interfaces * 3 * 2], //Extent geometry // planes: Vec::new(), } @@ -99,6 +101,10 @@ impl AInterfacesInfo { interfaces_id_from_cells[target_id * n_zones + source_id] = interface_id; let (plane, direction_neighbors) = grid.get_interface_plane(source_id, target_id); + + let origin = plane.origin.0; + let theta = origin[1].atan2(origin[0]); + self.interface_theta[interface_id] = theta; planes.push(plane); self.normal_axis[interface_id] = direction_neighbors; } @@ -106,6 +112,58 @@ impl AInterfacesInfo { self.global_id_from_interface = self.count_interfaces_second_pass(geometry, &interfaces_id_from_cells); self.fill_area(geometry, &planes); + + // for i_interface in 0..self.n_facet.len() { + // let total: f64 = self.area[i_interface].iter().sum(); + // if total > 0.0 { + // let source = self.ids[i_interface].source_id; + // let axis = self.normal_axis[i_interface]; + // let theoretical = grid.cell_surface(source, index_to_oriented(axis)); + // let factor = theoretical / total; + // for a in self.area[i_interface].iter_mut() { + // *a *= factor; + // } + // } + // } + + // self.check_areas(geometry); + } + #[allow(unused)] + fn check_areas(&self, geometry: &CMGeometry) { + let grid = geometry.get_grid().unwrap(); + let n_zones = geometry.n_zone(); + for cell_id in 0..n_zones { + for axis_idx in 0..3 { + let axis = crate::grid::index_to_oriented(axis_idx); + let theoretical_area = grid.cell_surface(cell_id, axis); + let total_area: f64 = self + .ids + .iter() + .enumerate() + .filter(|(i, id)| { + (id.source_id == cell_id || id.target_id == cell_id) + && self.normal_axis[*i] == axis_idx + }) + .map(|(i, _)| self.area[i].iter().sum::()) + .sum(); + + if ((total_area - theoretical_area).abs() / theoretical_area) < 0.1 { + println!( + "(areas): area incorect : axis: {}\r\n -cell_id:{}\r\n -total_area: {}\r\n -theoretical: {}", + axis_idx, cell_id, total_area, theoretical_area + ); + } + + // assert!( + // ((total_area - theoretical_area).abs() / theoretical_area) < 0.1, + // "RCMTOOL(areas): area incorect : axis: {}\r\n -cell_id:{}\r\n -total_area: {}\r\n -theoretical: {}", + // axis_idx, + // cell_id, + // total_area, + // theoretical_area + // ); + } + } } fn count_interfaces_second_pass( @@ -122,56 +180,28 @@ impl AInterfacesInfo { let grid = geometry.get_grid().unwrap(); let n_zones = geometry.n_zone(); - for (vol_element_global_id, interface_cid_0, interface_cid_k, k_vertex) in - geometry.interface_iter() - { - if k_vertex >= 1 - && grid.are_cell_neighbor(interface_cid_0, interface_cid_k) - != NeighborDirection::NotNeighbors - { - let interface_global_id = - interfaces_id_from_cells[interface_cid_0 * n_zones + interface_cid_k]; - let k_element = tmp_element_counter[interface_global_id]; - tmp_element_counter[interface_global_id] += 1; - global_id_from_interface[interface_global_id][k_element] = vol_element_global_id; + for (vol_element_global_id, _, interface_cid_k, k_vertex) in geometry.interface_iter() { + // if k_vertex >= 1 { + for i in 0..k_vertex { + let cid_i = geometry + .volume_elements + .get_list_compartment_id(vol_element_global_id, i); + let neighbors = grid.are_cell_neighbor(cid_i, interface_cid_k); + if neighbors != NeighborDirection::NotNeighbors { + let interface_global_id = + interfaces_id_from_cells[cid_i * n_zones + interface_cid_k]; + let k_element = tmp_element_counter[interface_global_id]; + tmp_element_counter[interface_global_id] += 1; + global_id_from_interface[interface_global_id][k_element] = + vol_element_global_id; + } } + // } } global_id_from_interface } - //fn count_interfaces_second_pass( - //&mut self, - //geometry: &CMGeometry, - //interfaces_id_from_cells: &[usize], - //) -> Vec> { - //let mut tmp_element_counter = vec![0; self.n_facet.len()]; - //let mut global_id_from_interface: Vec> = vec![Vec::new(); self.n_facet.len()]; - //for (element_id, n_element) in global_id_from_interface.iter_mut().zip(self.n_facet.iter()) - //{ - //*element_id = vec![0; *n_element]; - //} - //let grid = geometry.get_grid().unwrap(); - //let n_zones = geometry.n_zone(); - //let functor = |vol_element_global_id: usize, - //interface_cid_0: usize, - //interface_cid_k: usize, - //k_vertex: usize| { - //if k_vertex >= 1 - //&& grid.are_cell_neighbor(interface_cid_0, interface_cid_k)!= NeighborDirection::NotNeighbors - //{ - //let interface_global_id = - //interfaces_id_from_cells[interface_cid_0 * n_zones + interface_cid_k]; - //let k_element = tmp_element_counter[interface_global_id]; - //tmp_element_counter[interface_global_id] += 1; - //global_id_from_interface[interface_global_id][k_element] = vol_element_global_id; - //} - //}; - //geometry.interface_iterator(functor); - // - //global_id_from_interface - //} - fn fill_area(&mut self, geometry: &CMGeometry, planes: &[BoundedPlane]) { //This is almost the same algorithm as fill for c_info struct (to compute volume of velem) for (i, n) in self.n_facet.iter().enumerate() { @@ -191,11 +221,14 @@ impl AInterfacesInfo { geometry.fill_vertices(volume_element_global_id, n_vertex, &mut local_vertices); + // let area = compute_intersection_area(&local_vertices, elem_type, plane) + // .expect("Area between element"); + // let area = area * plane.normal.0[plane.axis].signum(); + // self.area[interface_id][i_facet] = area; + // let area = compute_intersection_area(&local_vertices, elem_type, plane) .expect("Area between element"); - if area == 0. { - println!("{} {}", interface_id, i_facet); - } + self.area[interface_id][i_facet] = area; } } diff --git a/cmtool-core/src/model/mod.rs b/cmtool-core/src/model/mod.rs index a3b22571..b35b7e36 100644 --- a/cmtool-core/src/model/mod.rs +++ b/cmtool-core/src/model/mod.rs @@ -1,17 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later +//We need to expose some grid interface to cfd-assemble +//TODO: refractor model to put cfd oriented into separated mod and move "reactor model" such as 0d/pfr from assemble to here + use crate::{ CoreError, - coordinates::{CartesianCoordinates, CartesianVec3, CylindricalCoordinates}, - grid::MeshType, + coordinates::{CartesianCoordinates, CartesianVec3, Coords3}, + errors::ModelError, model::{ compartments::{CompartmentInfo, CountVolumeElement, ElementVolumeInfo}, interfaces::{AInterfacesInfo, InterfaceFlow, InterfaceInfo}, }, }; -use std::sync::Arc; +use std::{f64, sync::Arc}; mod data; -use cmtool_data::{RawDataFlux, RawDataScalar}; +use cmtool_data::{FluxFileHeader, RawDataFlux, RawDataScalar, RawFlux}; mod compartments; mod geometry; mod interfaces; @@ -28,7 +31,109 @@ pub struct CMModel { pub use geometry::CMGeometry; +fn get_data_flow( + n_zones: usize, + i_info: &[InterfaceInfo], + i_flow: &[InterfaceFlow], +) -> RawDataFlux { + let fluxes: Vec = i_info + .iter() + .zip(i_flow) + .map(|(k_i_nfo, k_i_flow)| RawFlux { + id_source: k_i_nfo.source_id as u32, + id_target: k_i_nfo.target_id as u32, + flux_source_target: k_i_flow.source_flow, + flux_target_source: k_i_flow.target_flow, + }) + .collect(); + RawDataFlux { + header: FluxFileHeader { + n_fluxes: i_flow.len() as u32, + n_zone: n_zones as u32, + }, + fluxes, + } + + // let mut flux_field = RawDataFlux::new(n_zones, i_flow.len()); + // for (i_interface, rd) in flux_field.fluxes.iter_mut().enumerate() { + // let InterfaceInfo { + // source_id, + // target_id, + // } = i_info[i_interface]; + // rd.id_source = source_id as u32; + // rd.id_target = target_id as u32; + // let InterfaceFlow { + // source_flow, + // target_flow, + // } = &i_flow[i_interface]; + // rd.flux_source_target = *source_flow; + // rd.flux_target_source = *target_flow; + // } + + // flux_field +} + impl CMModel { + fn check_flux(n_zone: u32, rf: &RawFlux) -> bool { + let mut flag = false; + flag |= rf.flux_source_target.is_finite(); + flag |= rf.flux_source_target.is_sign_positive(); + flag |= rf.id_source < n_zone; + flag |= rf.id_target < n_zone; + flag + } + + pub fn check_flow(&self, raw: &RawDataFlux) -> Result<(), ModelError> { + const REL_TOLERANCE_DIVERGENCE_CELL: f64 = 1e-2; + const ABS_TOLERANCE_DIVERGENCE_CELL: f64 = 1e-7; + + let mut mass_balance: Vec = + vec![InterfaceFlow::default(); raw.header.n_zone as usize]; + let mut id_max = u32::MIN; + + for flow in raw.fluxes.iter() { + if !Self::check_flux(raw.header.n_zone, flow) { + return Err(ModelError::InvalidFlow); + } + + id_max = u32::max(id_max, flow.id_source); + //out + mass_balance[flow.id_source as usize].source_flow += flow.flux_target_source; + //in + mass_balance[flow.id_source as usize].target_flow += flow.flux_source_target; + + //in + mass_balance[flow.id_target as usize].source_flow += flow.flux_source_target; + //out + mass_balance[flow.id_target as usize].target_flow += flow.flux_target_source; + } + + for flow in &mass_balance { + let abs_diff = (flow.source_flow - flow.target_flow).abs(); + let denom = flow.source_flow.abs() + flow.target_flow.abs(); + + let err = if denom > f64::EPSILON { + 2.0 * abs_diff / denom + } else { + abs_diff + }; + if abs_diff > ABS_TOLERANCE_DIVERGENCE_CELL && err > REL_TOLERANCE_DIVERGENCE_CELL { + return Err(ModelError::CellToCellDivergence( + err, + REL_TOLERANCE_DIVERGENCE_CELL, + )); + } + } + + Ok(()) + } +} + +impl CMModel { + pub fn grid(&self) -> &dyn crate::grid::CompartmentMesh { + self.geometry.get_grid().unwrap() + } + pub fn init(geometry: Arc) -> Self { println!("Init model with {} compartment", geometry.n_zone()); let volume_element_count = geometry.get_count_volume_element_first_pass(); @@ -42,30 +147,26 @@ impl CMModel { geometry.n_zone() ) } + let interface_count_raw = volume_element_count.at_interface.clone(); let (c_info, interfaces) = volume_element_count.into_reduce(); - let n_max_interface = geometry.get_grid().as_ref().unwrap().n_maximum_interface(); - - if interfaces.n_interfaces() >= n_max_interface { - unimplemented!("should have intefaces < n_maximum_interface") + // if interfaces.n_interfaces() >= geometry.get_grid().as_ref().unwrap().n_maximum_interface() + if interfaces.n_interfaces() > geometry.get_grid().as_ref().unwrap().n_maximum_interface() { + unimplemented!( + "RCMTOOL: CMModel::init: should have intefaces < n_maximum_interface {} {}", + interfaces.n_interfaces(), + geometry.get_grid().as_ref().unwrap().n_maximum_interface() + ) } - // if interfaces.n_facet.len() != n_max_interface { - // eprintln!( - // "Intefaces should be n_maximum_interface {} {}", - // interfaces.n_facet.len(), - // n_max_interface - // ); - // // unimplemented!("Intefaces should be n_maximum_interface") - // } - let mut model = Self { geometry, c_info, interfaces, }; + model.c_info.fill(&model.geometry); model.interfaces.fill(&model.geometry, &interface_count_raw); @@ -73,71 +174,155 @@ impl CMModel { model } + #[allow(unused)] fn compute_volume_integral_per_zone() -> Vec { todo!() } + fn get_average_velocity( + &self, + vector: &Vector, + _curent_inteface_element: &[usize], + __curent_inteface_area: &[f64], + ) -> Coords3 { + let total_area: f64 = __curent_inteface_area.iter().sum(); + _curent_inteface_element + .iter() + .zip(__curent_inteface_area) + .fold([0.0f64; 3], |acc, (gid, a)| { + let v = CartesianVec3(vector.get_slice_xyz(*gid).to_owned()); + let CartesianCoordinates(centroid) = self.geometry.volume_elements.xyz[*gid]; + let theta = centroid[1].atan2(centroid[0]); + let cyl = v.to_cylindrical_vec(theta).0; + [ + acc[0] + cyl[0] * a / total_area, + acc[1] + cyl[1] * a / total_area, + acc[2] + cyl[2] * a / total_area, + ] + }) + } + pub fn compute_flux_between_compartments( &self, vector: Vector, ) -> Result { let n_fluxes = self.interfaces.n_interfaces(); - let mut flux_field = RawDataFlux::new(self.geometry.n_zone(), n_fluxes); - let mut flows: Vec = vec![Default::default(); n_fluxes]; for (i_interface, flow) in flows.iter_mut().enumerate() { let axis: usize = self.interfaces.normal_axis[i_interface]; - let current_interface_area = &self.interfaces.area[i_interface]; - let curent_inteface_element = &self.interfaces.global_id_from_interface[i_interface]; - for (global_id, area) in curent_inteface_element.iter().zip(current_interface_area) { - let vector_value = CartesianVec3(vector.get_slice_xyz(*global_id).to_owned()); + let axis_oriented = crate::grid::index_to_oriented(axis); - let coords = if self.geometry.mesh_type == MeshType::Cylindrical { - let CartesianCoordinates(centroid) = - self.geometry.volume_elements.xyz[*global_id]; + let source_id = self.interfaces.ids[i_interface].source_id; + let theoretical_area = self + .geometry + .get_grid() + .unwrap() + .cell_surface(source_id, axis_oriented); - let CylindricalCoordinates(centroid) = CartesianCoordinates(centroid).into(); + let current_interface_area = &self.interfaces.area[i_interface]; + let curent_inteface_element = &self.interfaces.global_id_from_interface[i_interface]; + let v_avg = + self.get_average_velocity(&vector, curent_inteface_element, current_interface_area); - vector_value.to_cylindrical_vec(centroid[1]).0 - } else { - vector_value.0 - }; + let f = v_avg[axis] * theoretical_area; - let f = coords[axis] * area; - if f > 0. { - flow.source_flow += f - } else if f < 0. { - flow.target_flow += f.abs() - } + if f > 0. { + flow.source_flow += f + } else if f < 0. { + flow.target_flow += f.abs() } } - for (i_interface, rd) in flux_field.fluxes.iter_mut().enumerate() { - let InterfaceInfo { - source_id, - target_id, - } = self.interfaces.ids[i_interface]; - - rd.id_source = source_id as u32; - rd.id_target = target_id as u32; - - let InterfaceFlow { - source_flow, - target_flow, - } = &flows[i_interface]; - rd.flux_source_target = *source_flow; - rd.flux_target_source = *target_flow; - } + self.clean(&mut flows); + + let data_flow = get_data_flow(self.geometry.n_zone(), &self.interfaces.ids, &flows); - Ok(flux_field) + self.check_flow(&data_flow)?; + Ok(data_flow) } // pub fn export_volume_integral_per_zone(&self,scalar:&mut cmtool_data::RawDataScalar) { // todo!() // } + fn clean(&self, flows: &mut [InterfaceFlow]) { + // const TOL: f64 = 1e-19; + const ABS_TOL_CONV: f64 = 1e-12; + const REL_TOL_CONV: f64 = 1e-6; + + const MAX_IT: usize = 10000; + let mut balance = vec![0.0f64; self.geometry.n_zone()]; + let n_interfaces_per_zone = { + let mut tmp = vec![0usize; self.geometry.n_zone()]; + for i_interface in 0..flows.len() { + let source_id = self.interfaces.ids[i_interface].source_id; + let target_id = self.interfaces.ids[i_interface].target_id; + tmp[source_id] += 1; + tmp[target_id] += 1; + } + tmp + }; + + // let f_err = |_balance: &mut [f64], _flows: &[InterfaceFlow]| { + // for (i_interface, flow) in _flows.iter().enumerate() { + // let source_id = self.interfaces.ids[i_interface].source_id; + // let target_id = self.interfaces.ids[i_interface].target_id; + // _balance[source_id] += flow.target_flow - flow.source_flow; + // _balance[target_id] += flow.source_flow - flow.target_flow; + // } + // _balance.iter().map(|x| x.abs()).fold(0.0f64, f64::max) + // }; + + let f_err = |_balance: &mut [f64], _flows: &[InterfaceFlow]| { + for (i_interface, flow) in _flows.iter().enumerate() { + let source_id = self.interfaces.ids[i_interface].source_id; + let target_id = self.interfaces.ids[i_interface].target_id; + _balance[source_id] += flow.target_flow - flow.source_flow; + _balance[target_id] += flow.source_flow - flow.target_flow; + } + // let total_balance = _balance.iter().map(|&x| x.abs()).sum::(); + let total_balance = _balance.iter().map(|&x| x * x).sum::().sqrt(); + let total_flow = _flows + .iter() + .map(|flow| flow.target_flow + flow.source_flow) + .sum::(); + + if total_flow > f64::EPSILON { + total_balance / total_flow + } else { + total_balance + } + }; + + let mut conv = false; + let mut it = 0; + let mut err = 0.; + + while it < MAX_IT && !conv { + balance.fill(0.0); + let max_err = f_err(&mut balance, flows); + + let delta = (err - max_err).abs(); + + conv = delta < ABS_TOL_CONV || delta / err.max(f64::EPSILON) < REL_TOL_CONV; + if conv { + break; + } + err = max_err; + it += 1; + for (i_interface, flow) in flows.iter_mut().enumerate() { + let source_id = self.interfaces.ids[i_interface].source_id; + let target_id = self.interfaces.ids[i_interface].target_id; + let corr = (balance[source_id] - balance[target_id]) + / (n_interfaces_per_zone[source_id] + n_interfaces_per_zone[target_id]) as f64; + flow.source_flow += corr; + flow.target_flow -= corr; + } + } + } + pub fn export_volume_integral_per_zone( &self, model_scalar: Scalar, diff --git a/cmtool-core/src/model/scalar.rs b/cmtool-core/src/model/scalar.rs index 8814d407..4c033678 100644 --- a/cmtool-core/src/model/scalar.rs +++ b/cmtool-core/src/model/scalar.rs @@ -2,13 +2,16 @@ use std::ops::Index; +use cmtool_data::ScalarValueType; + use crate::{ + CoreError, ensight_gold::{self, types::ElementsType}, model::CMGeometry, }; pub struct Scalar { - value_in_vo: Vec, + pub(crate) value_in_vo: Vec, pub name: String, } @@ -44,6 +47,36 @@ impl Scalar { name: eg_scalar.get_name().to_string(), } } + + pub fn element_wise(self, a: &Self) -> Result { + if a.value_in_vo.len() != self.value_in_vo.len() { + return Err(CoreError::Custom(format!( + "Bad size for scalar scaling {} vs {} ", + self.value_in_vo.len(), + a.value_in_vo.len() + ))); + } + + let values: Vec = a + .value_in_vo + .iter() + .zip(&self.value_in_vo) + .map(|(a, b)| a * b) + .collect(); + Ok(Self { + value_in_vo: values, + name: format!("{} per {} scalar ", self.name, a.name), + }) + } + + pub fn scalar_shift(self, lambda: ScalarValueType) -> Self { + let value_in_vo = self.value_in_vo.iter().map(|i| lambda - i).collect(); + + Self { + value_in_vo, + name: format!("{} shifted by {} ", self.name, lambda), + } + } } impl Index for Scalar { diff --git a/cmtool-core/src/model/vectors.rs b/cmtool-core/src/model/vectors.rs index 41f4a2d2..827b5e08 100644 --- a/cmtool-core/src/model/vectors.rs +++ b/cmtool-core/src/model/vectors.rs @@ -3,7 +3,7 @@ use crate::{ CoreError, ensight_gold::{self, types::ElementsType}, - model::CMGeometry, + model::{CMGeometry, Scalar}, }; pub struct Vector { @@ -18,6 +18,26 @@ impl Vector { .expect("Slice with exactly 3 elements") } + pub fn scale_by(self, a: Scalar) -> Result { + // if self.value_in_vo.len() != a.value_in_vo.len() { + // return Err(CoreError::Custom(format!( + // "Bad size for vector scaling {} vs {} ", + // self.value_in_vo.len(), + // a.value_in_vo.len() + // ))); + // } + + let values: Vec = self + .value_in_vo + .chunks(3) + .zip(&a.value_in_vo) + .flat_map(|(triplet, &s)| triplet.iter().map(move |&v| v * s)) + .collect(); + Ok(Self { + value_in_vo: values, + }) + } + pub(crate) fn from_scalar( scalars: [ensight_gold::scalar::ScalarField; 3], geometry: &CMGeometry, diff --git a/cmtool-core/src/utils/area.rs b/cmtool-core/src/utils/area.rs index 1f1b2067..799aa4c4 100644 --- a/cmtool-core/src/utils/area.rs +++ b/cmtool-core/src/utils/area.rs @@ -5,78 +5,107 @@ use crate::{ ensight_gold::types::{ElementsType, VolumeElementTypes}, }; -fn sort_polygon_ccw(points: &[[f64; 2]]) -> Vec<[f64; 2]> { +// fn sort_polygon_ccw(points: &[[f64; 2]]) -> Vec<[f64; 2]> { +// let centroid = { +// let (mut sx, mut sy) = (0.0, 0.0); +// for p in points { +// sx += p[0]; +// sy += p[1]; +// } +// [sx / points.len() as f64, sy / points.len() as f64] +// }; + +// let mut sorted = points.to_vec(); +// sorted.sort_by(|a, b| { +// let angle_a = (a[1] - centroid[1]).atan2(a[0] - centroid[0]); +// let angle_b = (b[1] - centroid[1]).atan2(b[0] - centroid[0]); +// angle_a.partial_cmp(&angle_b).unwrap() +// }); +// sorted +// } + +fn project_points_to_plane_2d(points: &[[f64; 3]], normal: &CartesianVec3) -> Vec<[f64; 2]> { + let n = normal.normalized(); + let arbitrary = if n.0[0].abs() < n.0[2].abs() { + CartesianVec3([1.0, 0.0, 0.0]) + } else { + CartesianVec3([0.0, 0.0, 1.0]) + }; + // let u = n.cross(&arbitrary).normalized(); + // let v = u.cross(&n).normalized(); + let v = n.cross(&arbitrary).normalized(); + let u = v.cross(&n).normalized(); + + points + .iter() + .map(|p| CartesianVec3::from_point_origin(CartesianCoordinates(*p))) + .map(|p| [p.dot(&u), p.dot(&v)]) + .collect() +} + +// fn sort_points_ccw_3d(points: &[[f64; 3]], normal: &CartesianVec3) -> Vec<[f64; 3]> { +// let projected = project_points_to_plane_2d(points, normal); +// let sorted_2d = sort_polygon_ccw(&projected); + +// let mut sorted_3d = Vec::with_capacity(points.len()); + +// for p2d in &sorted_2d { +// let idx = projected +// .iter() +// .enumerate() +// .min_by(|(_, a), (_, b)| { +// let da = (a[0] - p2d[0]).hypot(a[1] - p2d[1]); +// let db = (b[0] - p2d[0]).hypot(b[1] - p2d[1]); +// da.partial_cmp(&db).unwrap() +// }) +// .map(|(i, _)| i) +// .unwrap(); +// sorted_3d.push(points[idx]); +// } + +// sorted_3d +// } +// +fn sort_points_ccw_3d(points: &[[f64; 3]], normal: &CartesianVec3) -> Vec<[f64; 3]> { + let projected = project_points_to_plane_2d(points, normal); + let mut indices: Vec = (0..points.len()).collect(); let centroid = { let (mut sx, mut sy) = (0.0, 0.0); - for p in points { + for p in &projected { sx += p[0]; sy += p[1]; } - [sx / points.len() as f64, sy / points.len() as f64] + [sx / projected.len() as f64, sy / projected.len() as f64] }; - - let mut sorted = points.to_vec(); - sorted.sort_by(|a, b| { - let angle_a = (a[1] - centroid[1]).atan2(a[0] - centroid[0]); - let angle_b = (b[1] - centroid[1]).atan2(b[0] - centroid[0]); + indices.sort_by(|&a, &b| { + let angle_a = (projected[a][1] - centroid[1]).atan2(projected[a][0] - centroid[0]); + let angle_b = (projected[b][1] - centroid[1]).atan2(projected[b][0] - centroid[0]); angle_a.partial_cmp(&angle_b).unwrap() }); - sorted + + indices.iter().map(|&i| points[i]).collect() } -fn project_points_to_plane_2d(points: &Vec<[f64; 3]>, normal: &CartesianVec3) -> Vec<[f64; 2]> { +fn polygon_area_3d(points: &[Coords3], normal: &CartesianVec3) -> f64 { let n = normal.normalized(); - let arbitrary = CartesianVec3::from_point_origin(if n.0[0].abs() < 0.9 { - CartesianCoordinates([1.0, 0.0, 0.0]) - } else { - CartesianCoordinates([0.0, 1.0, 0.0]) - }); - let u = n.cross(&arbitrary).normalized(); - let v = n.cross(&u); + let mut area_vec = CartesianVec3([0.0, 0.0, 0.0]); + let n_pts = points.len(); - points - .iter() - .map(|p| CartesianVec3::from_point_origin(CartesianCoordinates(*p))) - .map(|p| [p.dot(&u), p.dot(&v)]) - .collect() -} + for i in 0..n_pts { + let p1 = CartesianVec3(points[i]); + let p2 = CartesianVec3(points[(i + 1) % n_pts]); -fn sort_points_ccw_3d(points: &Vec<[f64; 3]>, normal: &CartesianVec3) -> Vec<[f64; 3]> { - let projected = project_points_to_plane_2d(points, normal); - let sorted_2d = sort_polygon_ccw(&projected); - - let mut sorted_3d = Vec::with_capacity(points.len()); - - for p2d in &sorted_2d { - let idx = projected - .iter() - .enumerate() - .min_by(|(_, a), (_, b)| { - let da = (a[0] - p2d[0]).hypot(a[1] - p2d[1]); - let db = (b[0] - p2d[0]).hypot(b[1] - p2d[1]); - da.partial_cmp(&db).unwrap() - }) - .map(|(i, _)| i) - .unwrap(); - sorted_3d.push(points[idx]); - } + let cross = p1.cross(&p2); - sorted_3d + area_vec = area_vec.add(&cross); + } + 0.5 * (area_vec.dot(&n)).abs() } -// fn polygon_area_2d(points: &Vec<[f64; 2]>) -> f64 { -// let n = points.len(); -// let mut area = 0.0; -// for i in 0..n { -// let (x0, y0) = (points[i][0], points[i][1]); -// let (x1, y1) = (points[(i + 1) % n][0], points[(i + 1) % n][1]); -// area += x0 * y1 - x1 * y0; -// } -// area.abs() * 0.5 -// } - fn tetra_area(vertices: [CartesianCoordinates; 4], plane: &BoundedPlane) -> f64 { + const REL_TOL_DISTANCE: f64 = 1e-6; + const EPSILON: f64 = 1e-12; //f64::EPSILON let mut intersection_points = vec![]; let BoundedPlane { @@ -93,9 +122,24 @@ fn tetra_area(vertices: [CartesianCoordinates; 4], plane: &BoundedPlane) -> f64 .collect(); let mut points_on_plane = vec![]; - const TOL: f64 = 1e-1; + let edge_len = (0..4) + .flat_map(|i| (i + 1..4).map(move |j| (i, j))) + .map(|(i, j)| { + let e = [ + vertices[i].0[0] - vertices[j].0[0], + vertices[i].0[1] - vertices[j].0[1], + vertices[i].0[2] - vertices[j].0[2], + ]; + (e[0] * e[0] + e[1] * e[1] + e[2] * e[2]).sqrt() + }) + .fold(0.0_f64, f64::max); + if edge_len < EPSILON { + return 0.0; + } + let tol = REL_TOL_DISTANCE * edge_len; + for (i, dist) in distances.iter().enumerate() { - if dist.abs() < TOL { + if dist.abs() < tol { points_on_plane.push(vertices[i].0); } } @@ -104,9 +148,11 @@ fn tetra_area(vertices: [CartesianCoordinates; 4], plane: &BoundedPlane) -> f64 for j in (i + 1)..4 { let d1 = distances[i]; let d2 = distances[j]; - + if d1.abs() < tol || d2.abs() < tol { + continue; + } if d1 * d2 < 0.0 { - let t = d1 / (d1 - d2); + let t = d1.abs() / (d1.abs() + d2.abs()); let p1 = &vertices[i].0; let p2 = &vertices[j].0; let intersection = [ @@ -116,56 +162,54 @@ fn tetra_area(vertices: [CartesianCoordinates; 4], plane: &BoundedPlane) -> f64 ]; intersection_points.push(intersection); } + // if d1 * d2 < 0.0 { + // let t = d1.abs() / (d1.abs() + d2.abs()); + // let p1 = &vertices[i].0; + // let p2 = &vertices[j].0; + // let r1 = (p1[0].powi(2) + p1[1].powi(2)).sqrt(); + // let r2 = (p2[0].powi(2) + p2[1].powi(2)).sqrt(); + // let theta1 = p1[1].atan2(p1[0]); + // let theta2 = p2[1].atan2(p2[0]); + // let r_int = r1 + t * (r2 - r1); + // let theta_int = theta1 + t * (theta2 - theta1); + // intersection_points.push([ + // r_int * theta_int.cos(), + // r_int * theta_int.sin(), + // p1[2] + t * (p2[2] - p1[2]), + // ]); + // } } } - if intersection_points.len() < 3 { - if points_on_plane.len() >= 3 { - let filtered: Vec<_> = points_on_plane - .iter() - .cloned() - .filter(|p| plane.is_point_inside(CartesianCoordinates(*p))) - .collect(); - - if filtered.len() >= 3 { - let sorted = sort_points_ccw_3d(&filtered, normal); - return polygon_area_3d(&sorted, normal); - } else { - return 0.0; - } - } else { - return 0.0; + for p in &points_on_plane { + let already_present = intersection_points.iter().any(|q| { + let dx = q[0] - p[0]; + let dy = q[1] - p[1]; + let dz = q[2] - p[2]; + (dx * dx + dy * dy + dz * dz).sqrt() < tol + }); + if !already_present { + intersection_points.push(*p); } } + // intersection_points.extend(points_on_plane.iter().cloned()); - //Project points to 2D plane - // let projected = project_points_to_plane_2d(&intersection_points, normal); - - // //Sort points counterclockwise - // let sorted = sort_polygon_ccw(&projected); - - let sorted = sort_points_ccw_3d(&intersection_points, normal); - - //Compute area using shoelace formula - // return polygon_area_2d(&sorted); - polygon_area_3d(&sorted, normal) -} - -fn polygon_area_3d(points: &[Coords3], normal: &CartesianVec3) -> f64 { - let n = normal.normalized(); - - let mut area_vec = CartesianVec3([0.0, 0.0, 0.0]); - let n_pts = points.len(); - - for i in 0..n_pts { - let p1 = CartesianVec3(points[i]); - let p2 = CartesianVec3(points[(i + 1) % n_pts]); + if intersection_points.len() < 3 { + return 0.0; + } - let cross = p1.cross(&p2); + let filtered: Vec<_> = intersection_points + .iter() + .cloned() + .filter(|p| plane.is_point_inside(CartesianCoordinates(*p))) + .collect(); - area_vec = area_vec.add(&cross); + if filtered.len() < 3 { + return 0.0; } - 0.5 * (area_vec.dot(&n)).abs() + // let sorted = sort_points_ccw_3d(&intersection_points, normal); + let sorted = sort_points_ccw_3d(&filtered, normal); + polygon_area_3d(&sorted, normal) } pub fn compute_intersection_area( @@ -194,7 +238,11 @@ pub fn compute_intersection_area( #[cfg(test)] mod test { use super::*; - fn make_bounded_plane(normal: CartesianVec3, origin: CartesianCoordinates) -> BoundedPlane { + fn make_bounded_plane( + normal: CartesianVec3, + origin: CartesianCoordinates, + axis: usize, + ) -> BoundedPlane { // let (u, v) = orthonormal_basis(&normal); let extent = [-10.0, 10.0]; @@ -204,7 +252,7 @@ mod test { origin, extent_u: extent, extent_v: extent, - axis: 0, + axis, } } @@ -228,7 +276,7 @@ mod test { let expected_area = 0.5 * (cross_prod.0[0].powi(2) + cross_prod.0[1].powi(2) + cross_prod.0[2].powi(2)).sqrt(); - let plane = make_bounded_plane(normal, a); + let plane = make_bounded_plane(normal, a, 0); // let area = tetra_area([a, b, c, d], plane); @@ -254,7 +302,7 @@ mod test { let normal = CartesianVec3([0., 0., 1.]); let point = CartesianCoordinates([0., 0., 0.5]); - let plane = make_bounded_plane(normal, point); + let plane = make_bounded_plane(normal, point, 2); // Calculate the intersection area // let area = tetra_area( @@ -298,4 +346,14 @@ mod test { area ); } + + #[test] + fn test_sort_points_ccw_3d() { + let normal = CartesianVec3([0., 0., 1.]); + let points = vec![[0.5, 0.0, 0.5], [0.0, 0.5, 0.5], [0.0, 0.0, 0.5]]; + let sorted = sort_points_ccw_3d(&points, &normal); + assert_eq!(sorted[0], [0.0, 0.0, 0.5]); + assert_eq!(sorted[1], [0.5, 0.0, 0.5]); + assert_eq!(sorted[2], [0.0, 0.5, 0.5]); + } } diff --git a/cmtool-cxx/build.rs b/cmtool-cxx/build.rs index 3eed60ea..96f01fcc 100644 --- a/cmtool-cxx/build.rs +++ b/cmtool-cxx/build.rs @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + fn main() { cxx_build::bridge("src/lib.rs") .flag_if_supported("-std=c++20") diff --git a/cmtool-cxx/src/lib.rs b/cmtool-cxx/src/lib.rs index 439da0d5..0bc1118e 100644 --- a/cmtool-cxx/src/lib.rs +++ b/cmtool-cxx/src/lib.rs @@ -1,9 +1,17 @@ -use std::ptr::null; +// SPDX-License-Identifier: GPL-3.0-or-later + +//!Expose required method to C++ use cmtool_data::{ DiscontinuousTransitioner, FlowMapTransitioner, HydroState, IterationState, get_transitioner, }; use nalgebra_sparse::CooMatrix; + +use std::ptr::null; + +//Choice to use raw ptr was made to avoid clone/creating Arc which are useless for our usage +//All ptr are valid because they are created from rust side + struct TransitionerWrapper(DiscontinuousTransitioner); struct IterationStateWrapper(*const IterationState); @@ -49,8 +57,11 @@ mod ffi { fn has_gas(self: &IterationStateWrapper) -> bool; + #[allow(clippy::needless_lifetimes)] unsafe fn get_misc<'a>(self: &'a IterationStateWrapper, key: &str) -> &'a [f64]; + fn has_misc(self: &IterationStateWrapper, key: &str) -> bool; + fn flat_neighobrs(self: &IterationStateWrapper) -> &[usize]; fn n_compartments(self: &IterationStateWrapper) -> usize; @@ -154,7 +165,7 @@ impl TransitionerWrapper { fn get_dtransitioner(root: &str) -> Result, String> { match get_transitioner(root) { Ok(t) => Ok(Box::new(TransitionerWrapper(t))), - Err(d) => Err(format!("{}", d)), + Err(d) => Err(format!("Error while reading {}: {}", root, d)), } } @@ -173,6 +184,9 @@ impl IterationStateWrapper { None => Box::new(HydroStateWrapper(null())), } } + fn has_misc(self: &IterationStateWrapper, key: &str) -> bool { + unsafe { &*self.0 }.get(key).is_some() + } fn get_misc(self: &IterationStateWrapper, key: &str) -> &[f64] { unsafe { &*self.0 }.get(key).unwrap() diff --git a/cmtool-data/Cargo.toml b/cmtool-data/Cargo.toml index a0c5c4ff..ee300d78 100644 --- a/cmtool-data/Cargo.toml +++ b/cmtool-data/Cargo.toml @@ -14,7 +14,7 @@ nalgebra.workspace = true nalgebra-sparse.workspace=true ndarray.workspace=true serde.workspace = true -serde_cbor.workspace = true +#serde_cbor.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/cmtool-data/src/case.rs b/cmtool-data/src/case.rs index a23856f7..d1fe8e9f 100644 --- a/cmtool-data/src/case.rs +++ b/cmtool-data/src/case.rs @@ -25,8 +25,41 @@ pub struct CMCase { pub is_reursive: bool, } +impl std::fmt::Display for CMCase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "CMCase Configuration:")?; + writeln!( + f, + " - Number of Divisions: [{}x{}x{}]", + self.n_div[0], self.n_div[1], self.n_div[2] + )?; + writeln!(f, " - Description: {}", self.description)?; + writeln!( + f, + " - Time per Flow Map: {:.2} seconds", + self.time_per_flow_map + )?; + + // Show paths, iterating over the HashMap + writeln!(f, " - Export Paths:\n")?; + for export_type in self.paths.keys() { + writeln!(f, " - {:?}\n", export_type)?; + } + + writeln!( + f, + " - Recursive: {}", + if self.is_reursive { "Yes" } else { "No" } + )?; + Ok(()) + } +} + impl CMCase { pub fn n_compartment(&self) -> u32 { + if self.n_div.contains(&0) { + return 1; //FIXME + } self.n_div.iter().product() } @@ -34,6 +67,10 @@ impl CMCase { self.is_reursive = !self.is_reursive; } + pub fn is_two_phase_flow(&self) -> bool { + self.paths.contains_key(&CMAExportType::GasVolume) + } + pub fn add(&mut self, stype: CMAExportType, relative_path: &str) { self.paths.insert(stype, relative_path.to_string()); } @@ -89,13 +126,15 @@ impl CMCase { let ok_gas = if has_gas_volume && !has_gas_flow { false } else { - !(has_gas_flow && !has_gas_volume) + // !(has_gas_flow && !has_gas_volume) + !has_gas_flow || has_gas_volume }; let ok_liq = if has_liq_volume && !has_liq_flow { false } else { - !(has_liq_flow && !has_liq_volume) + // !(has_liq_flow && !has_liq_volume) + !has_liq_flow || has_liq_volume }; ok_liq && ok_gas diff --git a/cmtool-data/src/descriptors.rs b/cmtool-data/src/descriptors.rs index fb756027..bfcae8fd 100644 --- a/cmtool-data/src/descriptors.rs +++ b/cmtool-data/src/descriptors.rs @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later use serde::{Deserialize, Serialize}; + +///Legacy type of exported type value in a CM model +// #[deprecated(since = "0.1.4", note = "use CMExportType")] +//TODO remove where possible #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone, Copy)] pub enum CMAExportType { LiquidFlow, @@ -12,6 +16,7 @@ pub enum CMAExportType { Other, } +///Phase name in compartment model #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone, Copy)] pub enum PhaseCM { Liquid, @@ -37,11 +42,12 @@ impl From for String { } impl From for PhaseCM { - fn from(value: String) -> Self { + fn from(_value: String) -> Self { todo!() } } +///Enum-Struct based improvement of 'CMAExportType' pub enum CMExportType { Flow(PhaseCM), Volume(PhaseCM), diff --git a/cmtool-data/src/flowmap.rs b/cmtool-data/src/flowmap.rs index 5e5d81c7..bd64611d 100644 --- a/cmtool-data/src/flowmap.rs +++ b/cmtool-data/src/flowmap.rs @@ -4,7 +4,7 @@ use crate::{DataError, RawData, RawFlux, rawdata}; pub struct FlowMapDescriptor { pub flowmap: Array2, - pub(crate) neighbors: Array2, //TODO + pub neighbors: Array2, //TODO pub volumes: Vec, } @@ -26,31 +26,36 @@ impl FlowMapDescriptor { let n_zone = data_flows.header.n_zone as usize; let mut flowmap = Array2::::zeros((n_zone, n_zone)); - let mut neighbors: Vec> = vec![Vec::new(); n_zone]; //Vec::with_capacity(data.header.n_zone as usize) + let mut neighbors: Vec> = vec![Vec::with_capacity(10); n_zone]; if data_flows.header.n_zone != data_volumes.header.n_zone { return Err(DataError::BadData); } - for RawFlux { + let mut add_at = |i: usize, j: usize, val: f64| -> Result<(), DataError> { + //Error should never be triggered, by construction i= 0.); + assert!(flux_target_source >= 0.); - if let Some(g) = flowmap.get_mut((id_source, id_target)) { - *g += flux_source_target; - } + add_at(id_source, id_target, flux_source_target)?; + add_at(id_target, id_source, flux_target_source)?; - if let Some(g) = flowmap.get_mut((id_target, id_source)) { - *g += flux_target_source; - } neighbors[id_source].push(id_target); - neighbors[id_target].push(id_source) + neighbors[id_target].push(id_source); } let max_size = neighbors @@ -60,11 +65,18 @@ impl FlowMapDescriptor { .ok_or(DataError::BadData)?; let mut neighbor_flat = Array2::::zeros((n_zone, max_size)); + + neighbor_flat.fill(n_zone + 1); //Any ghost neighbor will have value n+1 + for (i_zone, neighbors_for_zone) in neighbors.iter().enumerate() { for (i_n, id_neighbor) in neighbors_for_zone.iter().enumerate() { - *(neighbor_flat.get_mut((i_zone, i_n)).unwrap()) = *id_neighbor; + // *(neighbor_flat.get_mut((i_zone, i_n)).unwrap()) = *id_neighbor; + *(neighbor_flat + .get_mut((i_zone, i_n)) + .expect("Flat neighbor out of bound")) = *id_neighbor; } } + let volumes: Vec = data_volumes.values.iter().map(|v| v.value).collect(); Ok(FlowMapDescriptor { @@ -81,15 +93,17 @@ mod test { #[test] fn read_descriptor() { - let flow_cma = std::env::var("CUVE_SLDMSH_FLOW_PATH").unwrap(); + let _flow_cma = std::env::var("CUVE_SLDMSH_FLOW_PATH"); - let volume_cma = std::env::var("CUVE_SLDMSH_VOLUME_PATH").unwrap(); + let _volume_cma = std::env::var("CUVE_SLDMSH_VOLUME_PATH"); - let descriptor = FlowMapDescriptor::from_path(flow_cma, volume_cma).unwrap(); + if let (Ok(flow_cma), Ok(volume_cma)) = (_flow_cma, _volume_cma) { + let descriptor = FlowMapDescriptor::from_path(flow_cma, volume_cma).unwrap(); - assert!(!descriptor.volumes.is_empty()); - assert!(descriptor.flowmap.is_square()); - assert!(descriptor.volumes.len() == descriptor.flowmap.ncols()); - assert!(descriptor.neighbors.nrows() == descriptor.flowmap.ncols()); + assert!(!descriptor.volumes.is_empty()); + assert!(descriptor.flowmap.is_square()); + assert!(descriptor.volumes.len() == descriptor.flowmap.ncols()); + assert!(descriptor.neighbors.nrows() == descriptor.flowmap.ncols()); + } } } diff --git a/cmtool-data/src/lib.rs b/cmtool-data/src/lib.rs index 11c11304..ac9766c4 100644 --- a/cmtool-data/src/lib.rs +++ b/cmtool-data/src/lib.rs @@ -7,6 +7,7 @@ mod rawdata; mod states; mod transitioner; pub use case::{CCMCaseInfo, CMCase, CMCaseJson, CMCaseReader, CMCaseWriter, read_case}; +use core::f64; pub use descriptors::{CMAExportType, CMExportType, PhaseCM}; pub use flowmap::FlowMapDescriptor; pub use rawdata::{ @@ -17,6 +18,7 @@ pub use states::*; use std::io; use thiserror::Error; pub use transitioner::*; + /// Errors that can occur during data operations. /// /// This enum encapsulates various error conditions that might arise during @@ -50,19 +52,53 @@ pub enum DataError { } #[inline(always)] +#[allow(unused)] fn linear_index_row_major(_n_row: usize, n_col: usize, i: usize, j: usize) -> usize { i * n_col + j } #[inline(always)] +#[allow(unused)] fn linear_index_col_major(n_row: usize, _n_col: usize, i: usize, j: usize) -> usize { j * n_row + i } +///Create transitioner pub fn get_transitioner(root: &str) -> Result { let case_path = format!("{}/cma_case", root); let p = std::path::Path::new(&case_path); let case = read_case(p)?; + //Load all the case information into the iterator T::from_case(root, &case) } + +///Compute the smallest average residence time in compartment +pub fn get_min_residence_time(fmt: &T) -> f64 { + let n_states = fmt.size(); + let mut min_all = f64::MAX; + + for i_state in 0..n_states { + let state = fmt + .get_at(i_state) + .expect("Transitioner error: n_state != real stored states"); + + if state.liquid.out_flows.len() != state.liquid.volumes.len() { + panic!("Mismatched lengths between out_flows and volumes."); + } + + let min_i = state + .liquid + .out_flows + .iter() + .zip(state.liquid.volumes.iter()) + .map(|(&f, &v)| f / v) + .filter(|&x| x.is_finite()) + .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater)) + .expect("Should exist a minimum for the state"); + + min_all = f64::min(min_all, min_i); + } + + min_all +} diff --git a/cmtool-data/src/rawdata.rs b/cmtool-data/src/rawdata.rs index 77772db7..27791db9 100644 --- a/cmtool-data/src/rawdata.rs +++ b/cmtool-data/src/rawdata.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later +use crate::DataError; use crate::descriptors::{CMExportType, PhaseCM}; use serde::{Deserialize, Serialize}; use std::{ @@ -7,8 +8,9 @@ use std::{ io::{Read, Write}, path::{Path, PathBuf}, }; -pub type ScalarValueType = f64; -use crate::DataError; + +///Scalar type (float) +pub type ScalarValueType = f64; //TODO decide if this alias is needed // A trait for reading and writing raw data to and from storage. pub trait RawData: Sized { @@ -98,7 +100,7 @@ pub struct ScalarFileHeader { #[derive(Deserialize, Serialize, Clone, Copy)] pub struct RawScalar { /// The scalar value stored as a floating-point number. - pub value: f64, + pub value: ScalarValueType, } /// Represents a collection of raw scalar data along with its header. @@ -173,9 +175,9 @@ impl Default for RawFlux { } } -impl From for RawScalar { +impl From for RawScalar { #[inline(always)] - fn from(value: f64) -> Self { + fn from(value: ScalarValueType) -> Self { Self { value } } } @@ -191,13 +193,26 @@ impl RawDataScalar { } } -impl From> for RawDataScalar { - fn from(value: Vec) -> Self { +impl From> for RawDataScalar { + fn from(value: Vec) -> Self { + value.as_slice().into() + } +} + +// impl Into> for RawDataScalar { +// fn into(self) -> Vec { +// self.values.iter().map(|i| i.value).collect() +// } +// } + +impl From<&[ScalarValueType]> for RawDataScalar { + fn from(value: &[ScalarValueType]) -> Self { + let len: u32 = value.len().try_into().unwrap_or_else(|_| { + panic!("Array length is too large to convert into u32"); + }); Self { - header: ScalarFileHeader { - n_zone: value.len().try_into().unwrap(), - }, - values: value.into_iter().map(|i| i.into()).collect(), + header: ScalarFileHeader { n_zone: len }, + values: value.iter().copied().map(Into::into).collect(), } } } @@ -259,6 +274,10 @@ impl RawData for RawDataFlux { fluxes.push(RawFlux::from_bytes(&buffer, &mut offset)?); } + if fluxes.len() as u32 != header.n_fluxes { + return None; + } + Some(RawDataFlux { header, fluxes }) } @@ -274,6 +293,7 @@ impl RawData for RawDataFlux { Ok(()) } } + pub trait FromBytes: Sized { fn from_bytes(buffer: &[u8], offset: &mut usize) -> Option; } @@ -336,15 +356,15 @@ impl ToBytes for FluxFileHeader { impl FromBytes for RawScalar { fn from_bytes(buffer: &[u8], offset: &mut usize) -> Option { - if *offset + size_of::() > buffer.len() { + if *offset + size_of::() > buffer.len() { return None; } - let value = f64::from_le_bytes( - buffer[*offset..*offset + size_of::()] + let value = ScalarValueType::from_le_bytes( + buffer[*offset..*offset + size_of::()] .try_into() .unwrap(), ); - *offset += size_of::(); + *offset += size_of::(); Some(RawScalar { value }) } } @@ -384,6 +404,11 @@ impl FromBytes for RawFlux { .unwrap(), ); *offset += size_of::(); + + if flux_source_target < 0. || flux_target_source < 0. { + return None; + } + Some(RawFlux { id_source, id_target, @@ -567,14 +592,22 @@ mod tests { let raw_data_flux = RawDataFlux { header: FluxFileHeader { n_zone: 10, - n_fluxes: 100, + n_fluxes: 2, }, - fluxes: vec![RawFlux { - id_source: 1, - id_target: 2, - flux_source_target: 0.1, - flux_target_source: 2.71, - }], + fluxes: vec![ + RawFlux { + id_source: 1, + id_target: 2, + flux_source_target: 0.1, + flux_target_source: 2.71, + }, + RawFlux { + id_source: 1, + id_target: 2, + flux_source_target: 0.1, + flux_target_source: 2.71, + }, + ], }; let path = "./tes2t.raw"; diff --git a/cmtool-data/src/states.rs b/cmtool-data/src/states.rs index a9b99784..6ec60c2b 100644 --- a/cmtool-data/src/states.rs +++ b/cmtool-data/src/states.rs @@ -4,16 +4,35 @@ use crate::FlowMapDescriptor; use nalgebra_sparse::CooMatrix; use ndarray::Array2; -macro_rules! non_zero { - ($i:ident, $eps:expr) => { - ($i.abs() > $eps) +macro_rules! almost_equal { + ($i:expr, $base:expr, $eps:expr) => { + (($i - $base).abs() < $eps) }; } +// macro_rules! non_zero { +// ($i:expr, $eps:expr) => { +// ($i.abs() > $eps) +// }; +// } + +// macro_rules! near_one { +// ($i:expr, $eps:expr) => { +// almost_equal!($i, 1.0, $eps) +// }; +// } + +macro_rules! round_if_needed { + ($val:expr, $base:expr, $tol:expr) => {{ + if almost_equal!($val, $base, $tol) { + $base + } else { + $val + } + }}; +} fn get_transition_from_fm(fm: Array2) -> (CooMatrix, Vec) { let n_compartments: usize = fm.nrows(); - const EPS: f64 = 1e-7; - // let row_sum = fm.sum_axis(Axis(1)); let mut transition = CooMatrix::new(n_compartments, n_compartments); let mut row_sum = vec![0.; n_compartments]; @@ -22,9 +41,7 @@ fn get_transition_from_fm(fm: Array2) -> (CooMatrix, Vec) { for i_col in 0..n_compartments { if i_row != i_col { let val = *fm.get((i_row, i_col)).expect("Bad formated flowmap"); - if non_zero!(val, EPS) { - transition.push(i_row, i_col, val); - } + transition.push(i_row, i_col, val); row_sum[i_row] += val; } } @@ -32,10 +49,9 @@ fn get_transition_from_fm(fm: Array2) -> (CooMatrix, Vec) { (0..n_compartments).for_each(|i_row| { let val = row_sum[i_row]; - if non_zero!(val, EPS) { - transition.push(i_row, i_row, -val); - } + transition.push(i_row, i_row, -val); }); + (transition, row_sum) } @@ -43,34 +59,62 @@ fn get_transition_from_fm(fm: Array2) -> (CooMatrix, Vec) { fn get_probability(liquid_neighors: &Array2, transition: &CooMatrix) -> Array2 { use nalgebra_sparse::CscMatrix; + //TODO: this method is called even though there's no flow (0D) + //To allow correct behaviour, assert has the condition outflow==0 + //Find a way cleaner way to : skip test and do not trigger assert + + //TODO change assert to real real and return Result<> let shape = liquid_neighors.dim(); - let mut proba = Array2::::zeros(shape); + //Start with filled with one array to ensure that probability will be increasing + let mut proba = Array2::::ones(shape); let transition_csc = CscMatrix::from(transition); - + let ghost_neighor = shape.0 + 1; (0..shape.0).for_each(|i_compartment| { let mut cumsum = 0.; - let mut count_neighbor = 0; - liquid_neighors.row(i_compartment).for_each(|i_neighbor| { - if *i_neighbor != i_compartment { - let out_flow = transition_csc - .index_entry(i_compartment, i_compartment) - .into_value(); + let out_flow = round_if_needed!( + transition_csc + .index_entry(i_compartment, i_compartment) + .into_value() + .abs(), + 0., + 1e-12 + ); + + //TODO PFR lead to cumsum==-1 how to handle assertion ? + let mut count_neighbor = 0; + liquid_neighors.row(i_compartment).for_each(|&i_neighbor| { + if i_neighbor != ghost_neighor { let proba_out: f64 = if out_flow != 0. { transition_csc - .index_entry(i_compartment, *i_neighbor) + .index_entry(i_compartment, i_neighbor) .into_value() - / out_flow.abs() + / out_flow } else { 0. }; + debug_assert!(proba_out >= 0.); + //round to one if close enough + let p_cp = round_if_needed!(proba_out + cumsum, 1., 1e-8); + + *proba + .get_mut((i_compartment, count_neighbor)) + .expect("Probability out of bound") = p_cp; - let p_cp = proba_out + cumsum; - *proba.get_mut((i_compartment, count_neighbor)).unwrap() = p_cp; cumsum += proba_out; } count_neighbor += 1; }); + //TODO PFR lead to cumsum==-1 how to handle assertion ? + // Idea: + // let is_pfr = i_compartment == 0 || i_compartment == liquid_neighors.len() - 1; + //For PFR NEED TO REMOVE THISASSERT FIXME + // assert!( + // (cumsum - 1.0).abs() < 1e-10 || out_flow == 0., + // "compartment {} cumulative probability = {} < 1 (not conservative)", + // i_compartment, + // cumsum + // ); }); proba @@ -96,6 +140,11 @@ impl HydroState { pub fn n_compartments(&self) -> usize { self.volumes.len() } + + #[inline(always)] + pub fn total_volume(&self) -> f64 { + self.volumes.iter().sum() + } } impl From for HydroState { @@ -129,6 +178,10 @@ impl IterationState { let liquid_neighors = liq.neighbors.clone(); //Todo find way to remove clone let liq_state: HydroState = liq.into(); + + //TODO + // call this only if liq has flow (transition and neighbors), 0D maps do not have + //proba #[cfg(feature = "probability")] let liquid_cumulative_probability = get_probability(&liquid_neighors, &liq_state.transition); @@ -169,16 +222,17 @@ mod test { #[test] fn construct_itstate_liquid_only() { - let flow_cma = std::env::var("CUVE_SLDMSH_FLOW_PATH").unwrap(); + let _flow_cma = std::env::var("CUVE_SLDMSH_FLOW_PATH"); + let _volume_cma = std::env::var("CUVE_SLDMSH_VOLUME_PATH"); - let volume_cma = std::env::var("CUVE_SLDMSH_VOLUME_PATH").unwrap(); + if let (Ok(flow_cma), Ok(volume_cma)) = (_flow_cma, _volume_cma) { + let descriptor = FlowMapDescriptor::from_path(flow_cma, volume_cma).unwrap(); - let descriptor = FlowMapDescriptor::from_path(flow_cma, volume_cma).unwrap(); + let vol_ref = descriptor.volumes.clone(); - let vol_ref = descriptor.volumes.clone(); + let state = IterationState::new(descriptor, None, HashMap::new()); - let state = IterationState::new(descriptor, None, HashMap::new()); - - assert!(state.liquid.volumes == vol_ref); + assert!(state.liquid.volumes == vol_ref); + } } } diff --git a/cmtool-data/src/transitioner/mod.rs b/cmtool-data/src/transitioner/mod.rs index 707d6072..7e6923c2 100644 --- a/cmtool-data/src/transitioner/mod.rs +++ b/cmtool-data/src/transitioner/mod.rs @@ -30,7 +30,7 @@ use buffer::{FlowMapBuffer, read_descriptors}; /// /// # Example /// -/// ```no_run +/// ```ignore /// let transitioner = MyTransitioner::from_case("root/path", &case)?; /// let state = transitioner.advance(12.0, 0.1); /// println!("Current state: {:?}", state); @@ -107,9 +107,13 @@ pub trait FlowMapTransitioner { } } +///Type of available transitionner pub enum TransitionerType { + ///Time based discontinous transition, easier to manipulate Discontinuous, + ///Index based discontinous transitionner, alway keep current state Simple, + None, } @@ -140,7 +144,7 @@ pub enum TransitionerType { /// /// # Example /// -/// ```no_run +/// ```ignore /// let transitioner = DiscontinuousTransitioner { /// state_buffer: vec![Arc::new(state1), Arc::new(state2), Arc::new(state3)], /// time_per_flomap: 0.5, @@ -267,7 +271,7 @@ impl FlowMapTransitioner for SimpleTransitioner { &self.state_buffer[self.current_index] } - fn need_advance(&self, _current_time: f64, time_step: f64) -> bool { + fn need_advance(&self, _current_time: f64, _time_step: f64) -> bool { true //Actually needs to be alsways updated because of remaining_time } diff --git a/cmtool-python/Cargo.toml b/cmtool-python/Cargo.toml index 47877b26..41ffc386 100644 --- a/cmtool-python/Cargo.toml +++ b/cmtool-python/Cargo.toml @@ -15,6 +15,6 @@ crate-type = ["cdylib"] [dependencies] cmtool-data.workspace = true -numpy = "0.24.0" +numpy.workspace = true pyo3.workspace = true -nalgebra.workspace=true +nalgebra.workspace = true diff --git a/cmtool-python/pycmtool/__init__.py b/cmtool-python/pycmtool/__init__.py index 89fab8a4..68a7509b 100644 --- a/cmtool-python/pycmtool/__init__.py +++ b/cmtool-python/pycmtool/__init__.py @@ -41,7 +41,7 @@ def get_sparse_transition_matrix(it): - return sparse_array_from_triplet(*it.flowmap) + return sparse_array_from_triplet(*it.transition) def sparse_array_from_triplet( diff --git a/cmtool-python/pycmtool/export_vtk.py b/cmtool-python/pycmtool/export_vtk.py new file mode 100644 index 00000000..9ea3aa0c --- /dev/null +++ b/cmtool-python/pycmtool/export_vtk.py @@ -0,0 +1,233 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +""" +This file is part of Compartment Modelling Tool Project (CMT). + +Compartment Modelling Tool Project (CMT) is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Compartment Modelling Tool Project (CMT) is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Compartment Modelling Tool Project (CMT). If not, see . + +Please contact: + +- Casale Benjamin: casale@insa-toulouse.fr +""" + +__all__ = [] +import json +import os +from typing import Dict, List + +import numpy as np +import vtk +import vtkmodules.util.numpy_support as npvtk + +# from cmtool.wcma_read import RawDataFlux + +# def mk_point(coordinates): +# points = vtk.vtkPoints() +# for coordinate in coordinates: +# points.InsertNextPoint(coordinate) +# return points + + +# def save_points(filename,coordinates): +# points = mk_point(coordinates) +# polydata = vtk.vtkPolyData() +# polydata.SetPoints(points) + +# # Write the PolyData to a VTK XML file +# writer = vtk.vtkXMLPolyDataWriter() +# writer.SetFileTypeToBinary() +# writer.SetFileName(f"{filename}.vtp") +# writer.SetInputData(polydata) +# writer.Update() +# writer.Write() + + +# def write_vtk(filename,coordinates,*args): +# points = mk_point(coordinates) +# polydata = vtk.vtkPolyData() +# polydata.SetPoints(points) + +# for i in args: +# polydata.GetPointData().AddArray(i) + +# # Write the PolyData to a VTK XML file +# writer = vtk.vtkXMLPolyDataWriter() +# writer.SetFileName(filename) +# writer.SetInputData(polydata) +# writer.SetDataModeToBinary() +# writer.Update() +# writer.Write() + + +def mk_scalar(np_array_data: np.ndarray, scalar_name: str): + """! + @brief Make VTK scalar from numpy array. + + This function converts a numpy array into a VTK scalar array. It sets the number of components to 1 + and assigns the specified scalar name. + + @param np_array_data (numpy.ndarray): The input numpy array containing scalar data. + @param scalar_name (str): The name to assign to the VTK scalar array. + + @return vtk.vtkFloatArray: The VTK scalar array containing the data. + + @example + import numpy as np + from cmtool.vtk import mk_scalar + + number_of_cell = ... + scalar = np.zeros((number_of_cell,)) + mk_scalar(scalar,"my_scalar") + """ + scalar_array = npvtk.numpy_to_vtk(np_array_data) + scalar_array.SetNumberOfComponents(1) + scalar_array.SetName(scalar_name) + return scalar_array + + +def read_scalar(filename, name) -> np.ndarray: + """! + @brief Read scalar data from a VTK XML unstructured grid file. + + This function reads scalar data from a VTK XML unstructured grid file. It retrieves the scalar + data associated with the specified name from the cell data of the grid. + + @param filename str: The path to the VTK XML unstructured grid file. + @param name str: The name of the scalar data array to be retrieved. + + @return numpy.ndarray: The scalar data as a numpy array. + + @example + scalar_data = read_scalar("example.vtu", "Pressure") + """ + reader = vtk.vtkXMLUnstructuredGridReader() + reader.SetFileName(filename) + # Perform the read operation + reader.Update() + grid = reader.GetOutput() + vtk_array = grid.GetCellData().GetArray(name) + return npvtk.vtk_to_numpy(vtk_array) + + +def append_scalar(filename, destination, *args): + """! + @brief Append scalar arrays to an existing VTK XML unstructured grid file. + + This function reads an existing VTK XML unstructured grid file, appends the specified scalar arrays to the cell data, + and writes the modified grid to a new VTK XML file. + + @param filename str: The path to the input VTK XML unstructured grid file. + @param destination str: The path to the output VTK XML unstructured grid file. + @param args vtk.vtkDataArray: Variable-length list of VTK scalar arrays to append to the cell data. + + @return None + + @example + pressure_array = vtk.vtkFloatArray() + pressure_array.SetName("Pressure") + # ...populate pressure_array with data... + append_scalar("input.vtu", "output.vtu", pressure_array) + """ + # Read the input VTK XML unstructured grid file + reader = vtk.vtkXMLUnstructuredGridReader() + reader.SetFileName(filename) + reader.Update() + grid = reader.GetOutput() + + # Append scalar arrays to the cell data + for scalar_array in args: + grid.GetCellData().AddArray(scalar_array) + + # Write the modified grid to a new VTK XML file + writer = vtk.vtkXMLUnstructuredGridWriter() + writer.SetFileName(destination) + writer.SetInputData(grid) + writer.SetDataModeToBinary() + writer.Update() + writer.Write() + + +def write_json_series( + destination: str, name: str, file_entries: List[Dict[str, str]] +) -> None: + """! + @brief Write a JSON file for a VTK file series. + + This function creates a JSON file that describes a series of VTK files. It includes + the version of the file-series and a list of file entries provided by the user. + + @param destination str: The directory path where the JSON file will be saved. + @param name str: The base name for the JSON file (without extension). + @param file_entries list: A list of file entries (each entry is a dictionary) that specifies the files in the series. + + @return None + + @example + file_entries = [ + {"name": "step0.vtu", "time": 0.0}, + {"name": "step1.vtu", "time": 1.0} + ] + write_json_series("/path/to/destination", "series_name", file_entries) + """ + json_content = {"file-series-version": "1.0", "files": file_entries} + + # Write the JSON file + json_file_path = f"{destination}/{name}.vtu.series" + with open(json_file_path, "w") as json_file: + json.dump(json_content, json_file, indent=4) + + +def mk_series(filepath: str, destination: str, series_name: str, t: np.ndarray, *args): + """! + @brief Generate a series of VTK files and a corresponding JSON series file. + + This function creates a series of VTK XML unstructured grid files (.vtu) and a JSON file that describes + the series. For each time step provided in the `t` array, it generates a VTK file with appended scalar + data arrays and saves these files in the specified destination directory. It also generates a JSON file + to represent the file series. + + @param filepath str: The path to the input VTK XML unstructured grid file. + @param destination str: The directory where the series files and JSON descriptor will be saved. + @param series_name str: The base name for the series files and the JSON descriptor. + @param t numpy.ndarray: An array of time steps corresponding to each VTK file in the series. + @param args Variable-length argument list of tuples, where each tuple contains: + - data numpy.ndarray: The scalar data array to be appended to each VTK file. + - name str: The name of the scalar data array. + + @return None + + @example + t = np.array([0.0, 1.0, 2.0]) + pressure_data = np.random.random((3, 100)) # Example scalar data with 3 time steps and 100 cells + temperature_data = np.random.random((3, 100)) # Another example scalar data + mk_series("input.vtu", "output_directory", "simulation_series", t, (pressure_data, "Pressure"), (temperature_data, "Temperature")) + """ + file_entries = [] + n_t = len(t) + root = f"{destination}/{series_name}" + if not os.path.exists(root): + os.makedirs(root) + + for i in range(n_t): + vtp_result = f"{root}/{series_name}_{i}.vtu" + scalar = [] + for s in args: + data = s[0] + name = s[1] + scalar.append(mk_scalar(data[i], name)) + append_scalar(filepath, vtp_result, *scalar) + file_entries.append({"name": f"{series_name}_{i}.vtu", "time": t[i]}) + write_json_series(root, series_name, file_entries) + + +__all__.extend(["mk_series", "read_scalar", "write_json_series", "mk_scalar"]) diff --git a/cmtool-python/src/case.rs b/cmtool-python/src/case.rs index fd5237a1..e9313e2c 100644 --- a/cmtool-python/src/case.rs +++ b/cmtool-python/src/case.rs @@ -6,7 +6,8 @@ use pyo3::prelude::*; use crate::PythonError; //Use C compartible enum because Enum-Struct not supported by PyO3 -#[pyclass(name = "CMExportType")] +// #[pyclass(name = "CMExportType")] +#[pyclass(from_py_object, name = "CMExportType")] #[derive(Clone, Copy)] pub enum CMExportTypeWrapper { LiquidFlow, @@ -31,7 +32,8 @@ impl From for cmtool_data::CMAExportType { } } -#[pyclass(name = "CMCase")] +// #[pyclass(name = "CMCase")] +#[pyclass(from_py_object, name = "CMCase")] #[derive(Clone)] pub struct CMCaseWrapper(cmtool_data::CMCase); @@ -48,6 +50,9 @@ impl CMCaseWrapper { cmtool_data::CMCaseJson::write_case(c, p).map_err(PythonError::from)?; Ok(()) } + pub fn resolve(&self, root: &str, stype: CMExportTypeWrapper) -> Option { + self.0.resolve(root, stype.into()) + } } #[pyfunction] diff --git a/cmtool-python/src/lib.rs b/cmtool-python/src/lib.rs index cfdd6d3d..d1bba5e4 100644 --- a/cmtool-python/src/lib.rs +++ b/cmtool-python/src/lib.rs @@ -37,7 +37,8 @@ mod pycmtool { #[pymodule_export] use super::{ DiscontinuousTransitionerWrapper, IterationStateWrapper, get_transitioner, - read_flowmap, read_rawflow, read_rawscalar, + new_raw_flux, read_flowmap, read_rawflow, read_rawscalar, scalar_from_data, + vector_from_data, }; } diff --git a/cmtool-python/src/rd.rs b/cmtool-python/src/rd.rs index 1137f11a..65ed6831 100644 --- a/cmtool-python/src/rd.rs +++ b/cmtool-python/src/rd.rs @@ -1,10 +1,16 @@ +use cmtool_data::FluxFileHeader; use cmtool_data::RawData; +use cmtool_data::RawDataFlux; +use cmtool_data::RawDataScalar; +use cmtool_data::RawFlux; use numpy::PyArray1; use numpy::PyArray2; -use numpy::ndarray::{self}; +use numpy::PyUntypedArrayMethods; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; +/* Scalar */ -#[pyclass(name = "RawDataScalar")] +#[pyclass(name = "RawDataScalar", frozen)] pub struct RawDataScalarWrapper(cmtool_data::RawDataScalar); #[pymethods] impl RawDataScalarWrapper { @@ -16,21 +22,100 @@ impl RawDataScalarWrapper { #[getter] pub fn data(&self, py: Python<'_>) -> Py> { let values: Vec = self.0.values.iter().map(|f| f.value).collect(); + //from does not perform copy + let array = numpy::ndarray::Array1::from(values); + PyArray1::from_owned_array(py, array).unbind() + } - let array = ndarray::Array1::from(values); + pub fn write(&self, path: &str) -> PyResult<()> { + if self.0.write_raw(path).is_ok() { + Ok(()) + } else { + Err(PyValueError::new_err("Scalar not found")) + } + } +} - PyArray1::from_owned_array(py, array).unbind() +#[pyfunction] +pub fn read_rawscalar(path: &str) -> PyResult { + if let Some(sc) = cmtool_data::RawDataScalar::read_raw(path) { + Ok(RawDataScalarWrapper(sc)) + } else { + Err(PyValueError::new_err("Scalar not found")) } } +#[pyfunction] +pub fn scalar_from_data<'py>( + _py: Python<'py>, + x: numpy::PyReadonlyArrayDyn<'py, f64>, +) -> PyResult { + if x.shape().len() != 1 { + return Err(PyValueError::new_err("Input array must be 1D.")); + } + + if !x.is_contiguous() { + return Err(PyValueError::new_err( + "Input array must be contiguous in memory.", + )); + } + + match x.as_slice() { + Ok(slice) => { + //TODO Why this condition has been used ? + // if slice.len() != 1 { + // return Err(PyValueError::new_err( + // "Input array must contain exactly one element.", + // )); + // } + let scalar = RawDataScalar::from(slice); + Ok(RawDataScalarWrapper(scalar)) + } + Err(_) => Err(PyValueError::new_err( + "Failed to convert the array to a contiguous slice.", + )), + } +} + +/* Flows */ + +#[pyclass(from_py_object, name = "RawFlux")] +#[derive(Clone, Copy)] +pub struct RawFluxWrapper(cmtool_data::RawFlux); + +#[pyfunction] +pub fn new_raw_flux( + _py: Python<'_>, + id_source: usize, + id_target: usize, + flux_source_target: f64, + flux_target_source: f64, +) -> RawFluxWrapper { + RawFluxWrapper(RawFlux { + id_source: id_source as u32, + id_target: id_target as u32, + flux_source_target, + flux_target_source, + }) +} + #[pyclass(name = "RawDataFlux")] pub struct RawDataFluxWrapper(cmtool_data::RawDataFlux); + #[pymethods] impl RawDataFluxWrapper { #[getter] pub fn n_zone(&self) -> usize { self.0.header.n_zone as usize } + + pub fn write(&self, path: &str) -> PyResult<()> { + if self.0.write_raw(path).is_ok() { + Ok(()) + } else { + Err(PyValueError::new_err("Vector not found")) + } + } } #[pyfunction] @@ -38,9 +123,30 @@ pub fn read_rawflow(path: &str) -> RawDataFluxWrapper { RawDataFluxWrapper(cmtool_data::RawDataFlux::read_raw(path).unwrap()) } +// impl FromPyObject for &[RawFluxWrapper] +// { +// type Error= +// fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + +// } +// } + #[pyfunction] -pub fn read_rawscalar(path: &str) -> RawDataScalarWrapper { - RawDataScalarWrapper(cmtool_data::RawDataScalar::read_raw(path).unwrap()) +pub fn vector_from_data<'py>( + _py: Python<'py>, + n_zone: usize, + x: Vec, +) -> PyResult { + let value: Vec = x.iter().map(|i| i.0).collect(); + let rd = RawDataFlux { + header: FluxFileHeader { + n_fluxes: value.len() as u32, + n_zone: n_zone as u32, + }, + fluxes: value, + }; + + Ok(RawDataFluxWrapper(rd)) } #[pyclass(name = "FlowMapDescriptor")] @@ -71,9 +177,32 @@ impl FlowMapDescriptorWrapper { unsafe { PyArray2::borrow_from_array(flowmap, this.into_any()) } } + #[getter] + pub fn neighbors(this: Bound<'_, Self>) -> Bound<'_, PyArray2> { + let flowmap = &this.borrow().0.neighbors; + // SAFETY: + // - The returned NumPy array shares memory with the internal `flowmap` (Array2). + // - We use `borrow_from_array`, which ties the array's lifetime to the Python object (`this`). + // - This guarantees that the underlying Rust memory remains valid as long as Python holds the array. + // + // Critical Requirements: + // - `self.0.flowmap` must not be mutated in a way that causes memory reallocation (e.g., replacing it). + // For example, the following code is unsafe if it runs after `pyobject.flowmap` is accessed: + // + // fn drop(&mut self) { + // self.0.flowmap = Array2::zeros((1, 1)); // BAD: reallocates backing buffer + // } + // + // - Violating this invariant (e.g., replacing the array or shrinking it) while Python holds a reference + // will cause undefined behavior (likely a segmentation fault). + // + // - Only expose immutable views or ensure exclusive access if mutations are needed. + unsafe { PyArray2::borrow_from_array(flowmap, this.into_any()) } + } + #[getter] pub fn volumes(&self, py: Python<'_>) -> Py> { - let array = ndarray::Array1::from(self.0.volumes.clone()); + let array = numpy::ndarray::Array1::from(self.0.volumes.clone()); PyArray1::from_owned_array(py, array).unbind() } } diff --git a/cmtool-python/src/transitionner.rs b/cmtool-python/src/transitionner.rs index b5de2b55..57033969 100644 --- a/cmtool-python/src/transitionner.rs +++ b/cmtool-python/src/transitionner.rs @@ -1,31 +1,12 @@ use std::sync::Arc; -use cmtool_data::{CCMCaseInfo, CMCaseReader, DiscontinuousTransitioner, FlowMapTransitioner}; +use cmtool_data::{DiscontinuousTransitioner, FlowMapTransitioner}; use numpy::PyArray1; -use numpy::ndarray::{self}; use pyo3::prelude::*; #[pyclass(name = "DiscontinuousTransitioner")] pub struct DiscontinuousTransitionerWrapper(DiscontinuousTransitioner); -#[pyclass(name = "IterationState")] -pub struct IterationStateWrapper(Arc); - -#[pymethods] -impl IterationStateWrapper { - #[getter] - pub fn flowmap(&self, py: Python<'_>) -> (&[usize], &[usize], &[f64]) { - let t = &self.0.liquid.transition; - (t.row_indices(), t.col_indices(), t.values()) - } - - #[getter] - pub fn volumes(&self, py: Python<'_>) -> Py> { - let array = ndarray::Array1::from_vec(self.0.liquid.volumes.clone()); - PyArray1::from_owned_array(py, array).unbind() - } -} - #[pymethods] impl DiscontinuousTransitionerWrapper { fn advance(&mut self, current_time: f64, time_step: f64) -> IterationStateWrapper { @@ -54,6 +35,64 @@ impl DiscontinuousTransitionerWrapper { } } +#[pyclass(name = "IterationState", frozen)] +pub struct IterationStateWrapper(Arc); + +#[pyclass(name = "HydroState", frozen)] +pub struct HydroStateWrapper(*const cmtool_data::HydroState); + +unsafe impl Sync for HydroStateWrapper {} +unsafe impl Send for HydroStateWrapper {} + +#[pymethods] +impl HydroStateWrapper { + #[getter] + pub fn transition(&self, _py: Python<'_>) -> (&[usize], &[usize], &[f64]) { + let t = unsafe { (*self.0).get_transition() }; + (t.row_indices(), t.col_indices(), t.values()) + } + #[getter] + pub fn volumes(&self, py: Python<'_>) -> Py> { + let deref = unsafe { &*self.0 }; + let array = deref.get_volume(); + let r = PyArray1::from_slice(py, array); + r.unbind() + } +} + +#[pymethods] +impl IterationStateWrapper { + #[getter] + pub fn liquid(&self) -> HydroStateWrapper { + let l = &self.0.liquid; + HydroStateWrapper(l) + } + + #[getter] + fn n_compartments(&self) -> usize { + self.0.n_compartments() + } + + #[getter] + fn get_gas(&self) -> Option { + self.0.gas.as_ref().map(|t| HydroStateWrapper(t)) + } + + fn misc(&self, key: &str, py: Python<'_>) -> Option>> { + if let Some(opt_m) = self.0.get(key) { + let r = PyArray1::from_slice(py, opt_m); + let a = r.unbind(); + Some(a) + } else { + None + } + } + + fn has_gas(&self) -> bool { + self.0.gas.is_some() + } +} + #[pyfunction] pub fn get_transitioner(root: &str) -> DiscontinuousTransitionerWrapper { let t: DiscontinuousTransitioner = cmtool_data::get_transitioner(root).unwrap(); diff --git a/cmtool/Cargo.toml b/cmtool/Cargo.toml index 382a6ee3..3c6a3691 100644 --- a/cmtool/Cargo.toml +++ b/cmtool/Cargo.toml @@ -10,8 +10,10 @@ license.workspace = true clap.workspace = true cmtool-core.workspace = true cmtool-data.workspace = true +cmtool-assemble.workspace = true thiserror.workspace = true - +nalgebra.workspace = true +nalgebra-sparse.workspace=true [features] use_vtk = ["cmtool-core/use_vtk"] diff --git a/cmtool/src/args/mod.rs b/cmtool/src/args/mod.rs index de07b8e6..b3ca06a0 100644 --- a/cmtool/src/args/mod.rs +++ b/cmtool/src/args/mod.rs @@ -35,8 +35,7 @@ pub enum Mode { } #[derive(Parser, Clone)] -#[clap(name = "cmtool")] -pub struct GenArgs { +pub struct CfdGenerate { #[clap(flatten)] pub common: CommonArgs, @@ -44,25 +43,67 @@ pub struct GenArgs { pub mode: Mode, } +#[derive(Parser, Clone)] +pub struct XMLGenerate { + #[clap(short, long)] + pub descriptor_path: String, + #[clap(short, long)] + pub out_dir: Option, +} + +#[derive(Subcommand, Clone)] +pub enum AllModes { + Cfd(CfdGenerate), + Xml(XMLGenerate), +} + +#[derive(Parser, Clone)] +#[command( + name = "CMTool", + author, + version, + about = "Command line interface to generate Compartment Models", + help_template = "\ +{name} {version} + +{about} + +USAGE: + {usage} + +OPTIONS: +{options} + +COMMANDS: +{subcommands} + +By {author} +" +)] +pub struct GenArgs { + #[clap(subcommand)] + pub mode: AllModes, +} + impl GenArgs { - #[cfg(debug_assertions)] - pub fn get() -> Self { - GenArgs { - common: CommonArgs { - n_i: 3, - n_j: 3, - n_k: 3, - out: None, - verbose: true, - }, - mode: Mode::Auto(AutoArgs { - case_path: - "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.encas" - .to_string(), - }), - } - } - #[cfg(not(debug_assertions))] + // #[cfg(debug_assertions)] + // pub fn get() -> Self { + // GenArgs { + // common: CommonArgs { + // n_i: 3, + // n_j: 3, + // n_k: 3, + // out: None, + // verbose: true, + // }, + // mode: Mode::Auto(AutoArgs { + // case_path: + // "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.encas" + // .to_string(), + // }), + // } + // } + // #[cfg(not(debug_assertions))] pub fn get() -> Self { GenArgs::parse() } diff --git a/cmtool/src/lib.rs b/cmtool/src/lib.rs index 943574b4..3a6d2ab1 100644 --- a/cmtool/src/lib.rs +++ b/cmtool/src/lib.rs @@ -1,5 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later -mod generators; + mod sanitizer; -pub use generators::*; pub use sanitizer::*; +use thiserror::Error; +#[derive(Error, Debug)] +pub enum CmtoolError { + #[error("Cmtool: {0}")] + Data(#[from] cmtool_data::DataError), + + #[error("Cmtool: {0}")] + Core(#[from] cmtool_core::CoreError), + + #[error("Cmtool Assemble: {0}")] + Assemble(#[from] cmtool_assemble::CMError), + + #[error("Cmtool: {0}")] + Custom(String), +} diff --git a/cmtool/src/main.rs b/cmtool/src/main.rs index 20cd8548..1ca5c3df 100644 --- a/cmtool/src/main.rs +++ b/cmtool/src/main.rs @@ -1,31 +1,49 @@ // SPDX-License-Identifier: GPL-3.0-or-later use cmtool::CmtoolError; -use std::fmt::Write; +use cmtool_data::{RawData, RawDataFlux}; +// use std::fmt::Write; +// use std::fs; +use std::path::PathBuf; use std::{env, path::Path}; mod args; use args::*; +fn out_or_default(out: Option) -> String { + out.unwrap_or(format!("{}/../out/", env!("CARGO_MANIFEST_DIR"))) +} + fn main() -> Result<(), CmtoolError> { let args = GenArgs::get(); let mode = args.mode; match mode { - Mode::Auto(autoargs) => auto_main(args.common, autoargs), - - Mode::Manual(manual_args) => todo!(), + AllModes::Cfd(cfdargs) => match cfdargs.mode { + Mode::Auto(autoargs) => { + if let Err(e) = auto_main(cfdargs.common, autoargs) { + eprintln!("{}", e); + return Err(e); + } + Ok(()) + } + + Mode::Manual(_manual_args) => todo!(), + }, + AllModes::Xml(xml) => { + let path = PathBuf::from(out_or_default(xml.out_dir)); + cmtool_assemble::headless_generate(&xml.descriptor_path, path)?; + Ok(()) + } } } fn auto_main(common: CommonArgs, autoargs: AutoArgs) -> Result<(), CmtoolError> { let stem = Path::new(&autoargs.case_path) - .file_stem() // Gets "casename" as OsStr + .file_stem() .and_then(|s| s.to_str()) - .unwrap(); // Converts OsStr to &str + .unwrap(); - let root_dir = common - .out - .unwrap_or(format!("{}/../out/", env!("CARGO_MANIFEST_DIR"))); + let root_dir = out_or_default(common.out); let case = cmtool_core::ensight_gold::case::Case::read(&autoargs.case_path)?; @@ -43,84 +61,62 @@ fn auto_main(common: CommonArgs, autoargs: AutoArgs) -> Result<(), CmtoolError> .dump_all(format!("{}/{}", root_dir, stem), &case.root, &case.paths) .map_err(CmtoolError::Core)?; - #[cfg(feature = "use_vtk")] - handle.write_vtk(format!("{}/{}/cma_case.vtu", root_dir, stem)); - - let p1 = "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.scl1"; - let p2 = "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.scl2"; - let p3 = "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.scl3"; + handle.dump_real_volume(format!("{}/{}/vofL", root_dir, stem))?; + handle.dump_real_volume(format!("{}/{}/vtot", root_dir, stem))?; + + // let path_gas_f = case.paths.iter().find(|f| f.name == "gas_vof").unwrap(); + + // let liquid_fraction = handle + // .get_scalar(std::path::PathBuf::from(&case.root).join(&path_gas_f.filepath)) + // .unwrap() + // .scalar_shift(1.); + + // let manual_flowl = handle + // .vector_from_scalar( + // "/tmp/inputs/RESULTS.scl1", + // "/tmp/inputs/RESULTS.scl2", + // "/tmp/inputs/RESULTS.scl3", + // ) + // .unwrap() + // .scale_by(liquid_fraction) + // .unwrap(); + + // handle.dump_vector_raw(format!("{}/{}/flowL", root_dir, stem), manual_flowl)?; + + // handle.dump_vector_from_scalar( + // format!("{}/{}/flowL", root_dir, stem), + // "/tmp/sanofi/inputs/RESULTS.scl1", + // "/tmp/sanofi/inputs/RESULTS.scl2", + // "/tmp/sanofi/inputs/RESULTS.scl3", + // )?; + + // #[cfg(feature = "use_vtk")] + // handle.write_vtk(format!("{}/{}/cma_case.vtu", root_dir, stem)); + + let _f = cmtool::check_flows( + handle.grid(), + &RawDataFlux::read_raw("./out/RESULTS/flowL.raw").unwrap(), + ) + .unwrap(); + // println!("{}", f); + // std::fs::write("/tmp/checks.csv", f); // - let export = handle - .dump_vector_from_scalar(format!("{}/flowL", root_dir), p1, p2, p3) - .unwrap(); - - if let Some(mut check_csv) = cmtool::check_flows(&export) { - check_csv - .write_str(&format!("sanitize_{}.csv", stem)) - .unwrap(); - }; + // let f = cmtool::divergence_free( + // &RawDataFlux::read_raw("./out/cuve_sldmsh_initmrf/velocity.raw").unwrap(), + // ); + // f.write_raw("./out/cuve_sldmsh_initmrf/velocity2.raw") + // .unwrap(); + // let mut case = cmtool_data::CMCase::new( + // [common.n_i as u32, common.n_j as u32, common.n_k as u32], + // 0., + // None, + // false, + // ); + + // cmtool_data::CMCaseJson::write_case( + // case, + // std::path::Path::new(&format!("{}/{}/cma_case", root_dir, stem)), + // )?; Ok(()) - - // cmtool::check_flows(&RawDataFlux::read_raw( - // "./out/cuve_sldmsh_initmrf/axial_velocity.raw", - // ).unwrap()); - - // cmtool::check_flows(&RawDataFlux::read_raw( - // "/home/benjamin/Documents/thesis/cfd-cma/sanofi/raw/flowL.raw", - // ).unwrap()); } - -// fn main() { -// // #[cfg(debug_assertions)] -// let args = GenArgs { -// case_path: "/home/benjamin/Documents/thesis/cfd-cma/rushton/cuve_sldmsh_initmrf.encas" -// .to_string(), -// n_i: 10, -// n_j: 10, -// n_k: 5, -// out: None, -// }; - -// //#[cfg(not(debug_assertions))] -// //let args = GenArgs::parse(); - -// let stem = Path::new(&args.case_path) -// .file_stem() // Gets "mycase" as OsStr -// .and_then(|s| s.to_str()) -// .unwrap(); // Converts OsStr to &str - -// let case = cmtool_core::ensight_gold::Case::read(&args.case_path).unwrap(); - -// let root_dir = args -// .out -// .unwrap_or(format!("{}/../out/", env!("CARGO_MANIFEST_DIR"))); - -// std::fs::create_dir_all(&root_dir).unwrap(); - -// let handle = cmtool_core::CMHandle::init( -// [args.n_i, args.n_j, args.n_k], -// &case.root, -// &case.geometry_file_path, -// cmtool_core::grid::MeshType::Cylindrical, -// ) -// .unwrap(); - -// handle -// .dump_all(format!("{}/{}", root_dir, stem), &case.root, &case.paths) -// .unwrap(); -// handle -// .dump_real_volume(format!("{}/{}/total_volume", root_dir, stem)) -// .unwrap(); -// // let p1 = "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.scl1"; -// // let p2 = "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.scl2"; -// // let p3 = "/home/benjamin/Documents/thesis/cfd-cma/sanofi_cfd/inputs/RESULTS.scl3"; -// // // -// // let export = handle -// // .dump_vector_from_scalar(format!("{}/flowL", root_dir), p1, p2, p3) -// // .unwrap(); - -// // cmtool::check_flows(&export); - -// cmtool::check_flows(&RawDataFlux::read_raw("./out/cuve_sldmsh_initmrf/velocity.raw").unwrap()); -// } diff --git a/cmtool/src/sanitizer.rs b/cmtool/src/sanitizer.rs index 6646c680..d2f51e44 100644 --- a/cmtool/src/sanitizer.rs +++ b/cmtool/src/sanitizer.rs @@ -12,12 +12,24 @@ fn smape(a: f64, b: f64) -> f64 { (a - b).abs() / (a.abs() + b.abs()) } -pub fn check_flows(raw_flows: &cmtool_data::RawDataFlux) -> Option { +pub fn check_flows( + _grid: &dyn cmtool_core::grid::CompartmentMeshManip, + raw_flows: &cmtool_data::RawDataFlux, +) -> Option { let mut mass_balance: Vec<(f64, f64)> = vec![(0.0, 0.0); raw_flows.header.n_zone as usize]; + // let boundary = grid.get_boundary(); + for flow in raw_flows.fluxes.iter() { - mass_balance[flow.id_target as usize].0 += flow.flux_source_target; + //out + mass_balance[flow.id_source as usize].0 += flow.flux_target_source; + //in mass_balance[flow.id_source as usize].1 += flow.flux_source_target; + + //in + mass_balance[flow.id_target as usize].0 += flow.flux_source_target; + //out + mass_balance[flow.id_target as usize].1 += flow.flux_target_source; } let mut zone_relative_errors = Vec::with_capacity(raw_flows.header.n_zone as usize); @@ -27,9 +39,15 @@ pub fn check_flows(raw_flows: &cmtool_data::RawDataFlux) -> Option { let mut total_inflow = 0.0; let mut total_outflow = 0.0; let mut f = String::new(); + let mut n_zone = 0; writeln!(&mut f, "zone_id,int,out,relative_error_percent").unwrap(); for (i, (inflow, outflow)) in mass_balance.iter().enumerate() { + // let is_boundary = boundary.iter().find(|&&ci| ci == i); + // if is_boundary.is_some() { + // continue; + // } + n_zone += 1; let relative_error = smape(*inflow, *outflow); zone_relative_errors.push(relative_error); @@ -39,25 +57,36 @@ pub fn check_flows(raw_flows: &cmtool_data::RawDataFlux) -> Option { total_inflow += inflow; total_outflow += outflow; + // writeln!( + // &mut f, + // "{},{},{},{},{}", + // i + 1, + // inflow, + // outflow, + // relative_error * 100.0, + // is_boundary.is_some() + // ) + // .unwrap(); writeln!( &mut f, "{},{},{},{}", i + 1, inflow, outflow, - relative_error * 100.0 + relative_error * 100.0, ) .unwrap(); } writeln!(&mut f).unwrap(); - let mean_relative_error = total_relative_error / raw_flows.header.n_zone as f64; + + let mean_relative_error = total_relative_error / n_zone as f64; writeln!(&mut f, "metric,value").unwrap(); - writeln!(&mut f, "total_inflow,{:.6}", total_inflow).unwrap(); - writeln!(&mut f, "total_outflow,{:.6}", total_outflow).unwrap(); + writeln!(&mut f, "total_inflow,{:.8}", total_inflow).unwrap(); + writeln!(&mut f, "total_outflow,{:.8}", total_outflow).unwrap(); writeln!( &mut f, - "global_net_error_percent,{:.6}", + "global_net_error_percent,{:.8}", smape(total_inflow, total_outflow) * 100.0 ) .unwrap(); @@ -76,3 +105,48 @@ pub fn check_flows(raw_flows: &cmtool_data::RawDataFlux) -> Option { Some(f) } + +pub fn divergence_free(raw_flows: &cmtool_data::RawDataFlux) -> cmtool_data::RawDataFlux { + use nalgebra::{DMatrix, DVector}; + let n_zone = raw_flows.header.n_zone as usize; + let n_flux = raw_flows.fluxes.len() * 2; + let mut new_flows = raw_flows.clone(); + + let mut f_vec = DVector::from_element(n_flux, 0.0); + for (i, flow) in new_flows.fluxes.iter().enumerate() { + f_vec[i * 2] = flow.flux_source_target; + f_vec[i * 2 + 1] = flow.flux_target_source; + } + + let mut a_mat = DMatrix::zeros(n_zone, n_flux); + for (k, flow) in new_flows.fluxes.iter().enumerate() { + let s = flow.id_source as usize; + let t = flow.id_target as usize; + + a_mat[(s, k * 2)] = 1.0; + a_mat[(t, k * 2)] = -1.0; + + a_mat[(t, k * 2 + 1)] = 1.0; + a_mat[(s, k * 2 + 1)] = -1.0; + } + + let div = &a_mat * &f_vec; + + let a_at = a_mat.transpose(); + let aat_inv = match (a_mat.clone() * a_at.clone()).try_inverse() { + Some(inv) => inv, + None => panic!("Cannot invert A*A^T"), + }; + let delta_f = a_at * (aat_inv * (-div)); + + for (i, flow) in new_flows.fluxes.iter_mut().enumerate() { + flow.flux_source_target += delta_f[i * 2]; + flow.flux_target_source += delta_f[i * 2 + 1]; + + if flow.flux_source_target < 0.0 || flow.flux_target_source < 0.0 { + panic!("Negative flux encountered"); + } + } + + new_flows +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..bc318c19 --- /dev/null +++ b/deny.toml @@ -0,0 +1,165 @@ +[graph] +targets = [ +] +all-features = true +no-default-features = false + +[output] +feature-depth = 1 + +[advisories] +ignore = [ +] + +[licenses] +allow = [ + "GPL-3.0-or-later", + "Zlib", + "BSD-2-Clause", + "BSD-3-Clause", + "MIT", + "Unicode-3.0", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", +] + +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], crate = "adler32" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The package spec the clarification applies to +#crate = "ring" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ +# Each entry is a crate relative path, and the (opaque) hash of its contents +#{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overridden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overridden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" +# List of crates that are allowed. Use with care! +allow = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, +] +# If true, workspace members are automatically allowed even when using deny-by-default +# This is useful for organizations that want to deny all external dependencies by default +# but allow their own workspace crates without having to explicitly list them +allow-workspace = false +# List of crates to deny +deny = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, +] + +# List of features to allow/deny +# Each entry the name of a crate and a version range. If version is +# not specified, all versions will be matched. +#[[bans.features]] +#crate = "reqwest" +# Features to not allow +#deny = ["json"] +# Features to allow +#allow = [ +# "rustls", +# "__rustls", +# "__tls", +# "hyper-rustls", +# "rustls", +# "rustls-pemfile", +# "rustls-tls-webpki-roots", +# "tokio-rustls", +# "webpki-roots", +#] +# If true, the allowed features must exactly match the enabled feature set. If +# this is set there is no point setting `deny` +#exact = true + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite. +skip-tree = [ + #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies + #{ crate = "ansi_term@0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +[sources.allow-org] +# github.com organizations to allow git sources for +github = [] +# gitlab.com organizations to allow git sources for +gitlab = [] +# bitbucket.org organizations to allow git sources for +bitbucket = [] diff --git a/documentations/docbook/book.toml b/documentations/docbook/book.toml index 0cf84926..2d02a276 100644 --- a/documentations/docbook/book.toml +++ b/documentations/docbook/book.toml @@ -3,11 +3,26 @@ authors = ["Casale Benjamin"] language = "en" src = "src" title = "Compartment Modeling Tool" -heading-split-level = 4 - -[output.html] -mathjax-support = true +# heading-split-level = 4 [preprocessor.toc] command = "mdbook-toc" renderer = ["html"] + +[preprocessor.bib] +bibliography = "refs.bib" +backend = "csl" +csl-style = "apa" +title = "References" + + +[output.html] +mathjax-support = true + + +[output.html.playground] +editable = false +copyable = true +copy-js = true +line-numbers = false +runnable = false diff --git a/documentations/docbook/src/SUMMARY.md b/documentations/docbook/src/SUMMARY.md index 32c7cff9..a838b32a 100644 --- a/documentations/docbook/src/SUMMARY.md +++ b/documentations/docbook/src/SUMMARY.md @@ -1,6 +1,7 @@ # Summary - [Home Page](./main.md) +- [Getting Started](./getting_started.md) - [Context](./context.md) - [Components](./components.md) - [Data type](./datatype.md) @@ -8,5 +9,7 @@ - [CFD-To-CMA](./cfd_to_cma.md) - [Tutorial]() + - [Basic Compartment Model](./tuto/basic_io.md) - [Basic mixing example](./tuto/basic_mixing.md) + - [Write XML Compartment Model Case](./tuto/basic_xml.md) diff --git a/documentations/docbook/src/assets/cm_assemble.svg b/documentations/docbook/src/assets/cm_assemble.svg new file mode 100644 index 00000000..0fec5f8b --- /dev/null +++ b/documentations/docbook/src/assets/cm_assemble.svg @@ -0,0 +1,4 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT2zpcdTAwMTf+3l/BcL+WXFytR1K/XHUwMDA1XHUwMDAyaVgua8Pyzlx1MDAxZCYkhqQ4cUjMXHUwMDEy7vS/v7JcdTAwMDHLO1x0doBOYzqdWLJk2TrPc1x1MDAxNlx1MDAxZPm/L0tLy+5kaC1/W1q2XHUwMDFl2i2711x1MDAxObXul7965XfWaNxzXHUwMDA2uor452PndtT2r+y67nD87e+/TYtK2+k/tbJsq29cctyxvu5/+nxp6T//f13T63ht++LnpuXI86PbxkNcclx1MDAxZVF/3D098Zv6XHUwMDE3vVxmZmS13dbgyrZM1YMuZ4hcdTAwMDbnXHUwMDEzfU4pXHUwMDBmzu97XHUwMDFkt6vLsFBBWdfqXXVd71x1MDAxMbhcZlxun/r9toSCkrE7cq6tNcd2Rt7N/8KW92dufdFqX1+NnNtBJ7jGXHUwMDFktVx1MDAwNuNha6Sf1Vxcd9mz7UN34veu35d+N8uxe1x1MDAxYz9cdTAwMGaSxMqzWumbXnVcdTAwMDfW2HufOCh1hq12z/VeXHUwMDAwRuYpvFx1MDAxMVx1MDAwZVx1MDAxYlx1MDAxZP/V/2vGNGr1rYb37lx1MDAwN7e2XHUwMDFkXHUwMDE091x1MDAwNlx1MDAxZMt7o8stXHUwMDE0udug83y3l3kzk0KfS36ZsVuW1zGWoDhBXHUwMDAwZnKM8NBE4T/OwJcjTJFcdTAwMDRJXHUwMDAwXHUwMDBiM6pxTVx1MDAwYpDr93rZsseWmVx1MDAwMm9o63HhXG5cdTAwMGJYSMj2XHUwMDBm1vjlbr9t7ZO1Y7d/1nSrYit4zIigtUYj5345qPn1Na9faW/z9kNcdTAwMDNdsGq3tbZ3vvZj9XGlhH7P+qPB2arqNLvi6PtdrdnnV+yshH5P77Y27If1+uP1dq1h1SePe9DqTtfv8y8jR7fDTutpYrBcdTAwMTCYXHUwMDExJrFcdTAwMDaVmTq7N7iOXHUwMDBime20r81cXH5cdFxyOMZcbkPc3JhcdTAwMWPsNC/weZc210Rt21x1MDAxZLSnZlx1MDAwNUFccpx8UIQk6oVcdTAwMTUoTmFcdTAwMDUmzIVcdTAwMGJWiLBcdTAwMDIuzFxuQlx1MDAwMJJcdTAwMThCXCJiSIFDJilcYoJcdTAwMDTMhVx1MDAxNDZcdTAwMWH0bFxiN+div+Nsb/ZO1q5uhiWAXGbLg7s9uD88dJuHO2KtWa+Px7yEfnfubsQx/Lx+YEetOsU3R/260yuhX3Jwc+5utjb4oGufsJtcdTAwMTXn9Gx3XFxCv+LHbnW3tltcdTAwMWb1XHUwMDBmXHUwMDFjl463rs9cdTAwMWbXNksjXHUwMDFiwoGWRDZ8UmPrTte6vr/a3Vb1i9FPtXUxNdlcdTAwMDA1iPNNXHUwMDEwYmT5hWyYXHUwMDAxsrFAwLRbcE2Ea0hhruFYYcZcYjbWYIhrSLzwhWtASKqEXHUwMDEyplVJVFPATnhcdTAwMWRcblx1MDAwMiRQKFx1MDAwN1xu/1xc2yftzulBvXG4et24ZeNcdTAwMWb22X1cdTAwMTJcbvpcdTAwMWVxlUuiKpdglEBcdTAwMDHFOFx0g1x1MDAwNVxiMkBAp1x1MDAwNVx1MDAwMclcdTAwMDKBZlx1MDAxOIx52NBcdLlwmVxul1xuylx1MDAwNOH0LSCIPEpCUIFLXCLZXGaCXHUwMDFhXHUwMDE0XHUwMDBmnV5cdTAwMWNl5teSkVx1MDAxOP8k+P3v19SrQ2LoXHUwMDFkK9g0SIBt7LZG7qqelN7gKj5Wa9DJqPFbVT0gd61WYop1u8y6oWNPrvxcdHpccqm2Pblr11x1MDAxZlx1MDAxYVx1MDAxM7F//nP/rLqDhrX9KZFaXHUwMDAxzqUmSE5cYpIxXHUwMDA3XHUwMDFhQYVShlx1MDAxMddcdTAwMGVcdTAwMWNcIka2XHUwMDE3IH5cdTAwMGKIWWFcdTAwMTAzTCUliptcdTAwMDFcdTAwMWFcdTAwMTCzhClcdTAwMWRYzVIqKoHxN6myXHUwMDFjZVx1MDAwM4B5aCxcdTAwMGJcZlx1MDAxN8HwOb7aXHUwMDFinzZvdqpbXHUwMDBmrl1tws/26o/pMIwqTM+FwNpYUYRcdTAwMWI58OSWIVxcQXr+XHSmSuthyZIxsVx1MDAwNYpnQTEvjGKsXHUwMDE0UCapSDNIIdMglZiBnl54k++bi2KqwFx1MDAwNEBcdTAwMTcoLoLizaN6c6xcdTAwMDZccvt+XHUwMDAyt/Y+c+n51t50KGZcdTAwMTXCiFx1MDAwNIkpYlrzRlDMXHUwMDExrWhcdTAwMDArpjTOQVxio6dcdTAwMTcgflx1MDAwYoihOIgpRUJcbirTwtoyXHUwMDEzxFx1MDAxOIOGv8Zd+bpYqoU9XVx1MDAxMorXhVtcdTAwMDX62G6uy2p1sElcdTAwMWa5q7amQbFcZlx1MDAwNVx1MDAxOZ6CzUl1a4Q3XHUwMDE0ajZwXyA1glRR3GhGQlx1MDAxMaIg1fPN1rZcdTAwMWNxjJg0rUrzfIVEn8BqjlxcrVx1MDAwNfC3XHUwMDA06lx0c/dcdTAwMGX2bbKxsYVU7+Z4e1JcdTAwMWLfTVx1MDAwM1RFoFwiKKJcXKtU5PlHMdhCXHUwMDA1Y4FcdTAwMDBJIHrSUozmXHUwMDA1imdBsSyOYn/dR+E0dctcdTAwMTLQXHUwMDBlgriIXHUwMDEwriBkL5WkbSWT0lxixVx1MDAwMsNcdTAwMDUwnL9cdTAwMWVcdTAwMWaLhUdATFQ0zkxFMs6MkZkmY1x1MDAxN1dcdTAwMTBT5uAhfL9cdTAwMGVcXFx1MDAwYlFM8Vx1MDAxZlx1MDAwMlxcVVx1MDAxOLhKau/Sy1x1MDAwMUlTv4lAVmAnM0455kqZK0rTv0qQT+Dvhlx1MDAwNNM7VpIyWVx1MDAwNppDXHUwMDEz9Zx81ZhcItNKX913Or7IOaOLnlx1MDAxYq647D1YnT3vTURehH7iiue56knDklNGkKHqtFpcdTAwMDRcIvF8v6LPNy3vxKnBsi+c+6lWuPKX4vKoXHUwMDA3OKpcdTAwMDAn+lBIUCGNPHnAXHUwMDAzwFx1MDAxNYmBYsEkYZgkM05whVx1MDAxMiqYXHUwMDEyilxuXCKQYMZ0XGJYXG5ktFx1MDAxM6PEXHUwMDE2JFx1MDAxNSGpamGS0u9fXCLJWFpgXbB44Vx1MDAwYklcdNA2icCkfFx1MDAxZkEwzOQsy7hz4qhMKfWOlYSAzpOx8lx1MDAxMzPeyFhcdTAwMWOhSLxcIlx1MDAxOa54d07Co1x1MDAxM3F4sF6rN9mkKrd2XHUwMDFm3J1hSlx1MDAwZWzSpVx1MDAwMVx1MDAxZc09XHUwMDAxMIJcdTAwMWLQTkjxmeSTXHUwMDA1r6TzymrxKCFcdTAwMDOmXHRC8LTgQ9KXeSFcdTAwMTaCKFx1MDAwMsVcdTAwMTCdXHUwMDA3s4BcZoVCPopZVkKS6Fx1MDAxZMQ0eDt1vLvr0v7+fe+eVtcvuqfo+HB1c31wcLk5XHJWmYxj1eAywGpcdTAwMTKpXHUwMDBiXHUwMDEzIFx1MDAwYqprxaGKtf1NMFx1MDAxMqlQRfHCwFFcdTAwMDGMXHUwMDA1pYKX76hcYm1fzpTLNScjIKLzy1Hy747Ulc371thef+j9rG83uydHRzV7lUylVVnUtKdkqlgg5kb5LqBcdTAwMWGBaq2EtTetVDXq0pLHsYxcdTAwMTdcdTAwMDbWOlx1MDAxMYJcdTAwMTGq3lx1MDAxMlx1MDAwYnxcdTAwMDWogDD+eJ1cdTAwMWG5ekVL4G+J1PxM/Fxcn5xFXHUwMDE3zXFKOFCmYbWCvdw3goAgJDTnLlxm4lxm6K5cdTAwMTeGLlx1MDAwNaFcdTAwMDRFqbnYNFvJUiyJ/lx1MDAwN+XnoVxuqT3tWfZcdTAwMGXMXHUwMDA3uzJcdTAwMDbehEiWgeUs1zp/g9XSXHUwMDFig4FIcEFcdFAkJVx1MDAxN6HQSmp1ylx1MDAwM87H81x1MDAwZcr7zp2103tcdTAwMWH8+Ljndp9FZlxuisrf1JNHUVx1MDAxOMWcdM6TXHUwMDFiREJcdTAwMWFq4aO/RklcdTAwMWIlWFx1MDAxM4BcdTAwMTmjqSuLJHNlXHUwMDExK640g0j1lnS834KSVCR+vzJcdTAwMDNcdTAwMDNcdTAwMTXmmd5g3OtYU1x1MDAxMU2MVlx1MDAxNEGUckaUXHUwMDAw5SW85rBKSWbOO/BN/ma/PL6RwCpUaYHSbqogsf3xXHUwMDAwvFwiXGJcYuRt8FCCJ1cptFxuijRPW0tFXHUwMDE1onlcXFx0XHUwMDA2XGZcdTAwMTGmu6OzZEH8UVxcVS+BqyThXGIrlppcdTAwMDaRvW9WW12AqcBziCcqyrlcdTAwMTnNh8VcdTAwMTOzRdVvn1x1MDAxNNIyXGaqubFGXHUwMDBlXHUwMDE57KH920uwh8jZYienP3aazumJlSSDlNxEmGJ9IInvULB4XHUwMDAx51xinL9cdTAwMTeGM1x1MDAwN1xmNLxcdTAwMDZcdTAwMTXOIc5cdTAwMDKzXHUwMDEwXGKxOURcdTAwMWKVhs3HJ1x1MDAxMK9Ew42gPlx1MDAxNqdvXGZi5H+zI09jM1x1MDAxYVxyYkAoXHUwMDFlXHUwMDFkqGXtPXGmsNTeXHUwMDEzQlx1MDAxY1JyXHUwMDA3JKogpC0griTn4U1ar2OYXFwqi7E/XHUwMDA0w41cdTAwMTJ282BMmNY8aSpcdTAwMTlnb1x1MDAwNPBST/X8qfJDXHUwMDFhUr9cdTAwMTf+XHSQnCmmfm1cXELLwPn7ZjtcdTAwMDFnVOlHYF7iiEIxT0RSxlx1MDAwMSlcdTAwMDRcdTAwMWNzLlQym+vdM1x1MDAwYvI/+JNHSpzGv6yVjFqkmFx1MDAwZcrAc0E7XHUwMDEx2tksTjuMU6VlLJn77Fdm0o6GIVx1MDAxN5LQ8ldBJGZMfPy+hphcdTAwMDGhQlxyXiOWwvQxQ9xcdTAwMDJ5rjfhoOeQaJ+E0Ji/glx1MDAxMJWCK5BKXHUwMDEx/W5cdTAwMTHkXHUwMDA1SD/cKcn/bNArXHUwMDEx0eiGZcxVRT+t99lcdTAwMDGsfV2GkplcdTAwMTEqdVx1MDAxMSfSSIhZPun3R3HPVmHukVQwLFx1MDAxMUpdf822eFx1MDAxONKUXHUwMDE1Wfspj3q0/fVcdFwipvFFnLhIXHUwMDE2p6K5LNVoXHUwMDBiVjuVTHJcboibvatPlUhK4n1yQlx1MDAxYqtKz3rKqvIn4qH8z4zl8ZBC0b0kXHUwMDAwycVjVeFcdTAwMThcdTAwMTh//lx1MDAwM5JcdTAwMTYtWZg8XHUwMDE5tLNdfCdcdFx1MDAwNSpUhsWTuZNEm+dSXHUwMDExLOfAOrN+dm5OXHUwMDA2T6ZY+rUwg1x1MDAwMfRJlo4/Niv7y/NcdTAwMGJablxyh4eunvLg8Zfvetb9alx1MDAxMqR/XfqH96E2f2BcdTAwMWVALV+4f3359X9a81jmIn0= \ No newline at end of file diff --git a/documentations/docbook/src/assets/tuto_mixing.svg b/documentations/docbook/src/assets/tuto_mixing.svg new file mode 100644 index 00000000..e0eb4823 --- /dev/null +++ b/documentations/docbook/src/assets/tuto_mixing.svg @@ -0,0 +1,1788 @@ + + + + + + + + 2026-02-18T11:07:45.892998 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentations/docbook/src/cfd_to_cma.md b/documentations/docbook/src/cfd_to_cma.md index 9fc989eb..3c67a400 100644 --- a/documentations/docbook/src/cfd_to_cma.md +++ b/documentations/docbook/src/cfd_to_cma.md @@ -1 +1,4 @@ # CFD-To-CMA + +Work in progess partially working feature +According to @@computational_engineering_international_inc_ensight_2003, ... diff --git a/documentations/docbook/src/components.md b/documentations/docbook/src/components.md index 65f3524a..35766bf4 100644 --- a/documentations/docbook/src/components.md +++ b/documentations/docbook/src/components.md @@ -5,5 +5,30 @@ This crates aims to provides different feature to perform complete simulation us - Unified data type that can easily be written and read - A Modeler to generate predefined simple model - A CFD-to-CMA generator to use CFD results exported in Ensight-Gold Format +- Assemble flowmaps seamlessly +# Crate organization +## cmtool + +Simple CLI tool for fast use. +Generation from CFD case or from xml descriptor + +## cmtool-data + +Utilities to manipulation Compartment Data such as Flowmaps and Scalar Fields. High level manipultation is done with FlowMap transitioner, low-level is used to directly write Compartment data. + +## cmtool-core +CFD-to-CMA: Algorithm to transform results from fine-mesh transient CFD simuation into Compartment Models. + +## cmtool-assemble + +Generate flowmaps from from models and assemble them. Flowmaps descriptor as XML file can be used from high-level use. Direct generate and case writer is available for low-level use. + +## cmtool-python + +Python bindings, namely binds cmtool-core generation from Ensight case, FlowmapTransitioner and case reading. + +## cmtool-cxx + +Bindings for c++ use, expose FlowMapTranstioner API diff --git a/documentations/docbook/src/context.md b/documentations/docbook/src/context.md index 06ddf69b..6c1840db 100644 --- a/documentations/docbook/src/context.md +++ b/documentations/docbook/src/context.md @@ -8,20 +8,17 @@ For these reasons, simulating an entire process at an industrial scale is not st One method to reduce complexity is the Compartment Modeling Approach (CMA). -## Compartment Modeling Approach +## Compartment Modeling Approach + One method to reduce complexity is the **Compartment Modeling Approach** (CMA). -The idea is simple: if we have a very slow and long-driven reaction that does not significantly affect the reactor's spatial properties (such as flow, temperature, pressure), we can first solve the main reactor's behavior with very high spatial and temporal precision but for a small time range, like a typical CFD study (covering minutes, or perhaps a few hours, of physical time). We then keep these results and reduce the spatial resolution as needed by projecting the results onto a coarser grid. These fixed results are used for long-term simulations. + +Inspired by scaled-down reactors, @@delafosse_cfd-based_2014 , the idea is simple. For a slow and long-driven reaction and that does not significantly affect the reactor's spatial properties (such as flow, temperature, pressure), one can first solve the main reactor's behavior with very high spatial and temporal precision but for a small time range, like a typical CFD study (covering minutes, or perhaps a few hours, of physical time). Then keep these results and reduce the spatial resolution as needed by projecting the results onto a coarser grid. These fixed results are used for long-term simulations. The specificity of CMA is that we reduce the results such that we no longer consider individual "cell meshes" but rather compartments that represent large spatial regions in the reactor, which are considered homogeneous. Furthermore, while meshes can be unstructured with cells of any shape and size, compartments are arranged in a structured grid of regular tetrahedrons. ### Use cases -This obsviouly reduce results accuracy but for some specific application it can be very useful. Compartments keeps, the main average flow between reactors regions (very important for mixing study), the eulerian scalar fields (gas-fraction,temperature,turbulency) is kept but volume averaged. - -Follow some academic work - - - +Even tough results accuracy is reduced, it can be very useful for some specific application. Compartments keeps, the main average flow between reactors regions (very important for mixing study @@delafosse_eulerlagrange_2015 ), the eulerian scalar fields (gas-fraction,temperature,turbulency) is kept but volume averaged usefull for segregated reactors @@hristov_simplified_2004. diff --git a/documentations/docbook/src/datatype.md b/documentations/docbook/src/datatype.md index 124dfc01..0b87ace7 100644 --- a/documentations/docbook/src/datatype.md +++ b/documentations/docbook/src/datatype.md @@ -22,20 +22,20 @@ Flows are stored as binary data with a structure that can be described as "flow- The raw file structure is as follows: -- Header: - - Number of compartments - - Number of flows -- Body: - - List of flows: +|Offset| Size | Type | Description| +--::--- | :----: | ---- | ----------- +0x00 | 4 | uint32 | Number of compartments +0x04 | 4 | uint32 | Number of flows (n) +0x08 | $$ n \times 2 \times 8 $$ | binary | Body (compartment & flow data) + ## Scalars Scalar fields are more standard files, they contains the value of the considered sclar for each zone of our grid, id for each compartment. -- Header: - - Number of compartments -- Body: - - List of values: - +|Offset| Size | Type | Description| +--::--- | :----: | ---- | ----------- +0x00 | 4 | uint32 | Number of compartments (m) +0x08 | $$ m \times 8 $$ | binary | Body (scalar values) ## Case diff --git a/documentations/docbook/src/getting_started.md b/documentations/docbook/src/getting_started.md new file mode 100644 index 00000000..2de04d18 --- /dev/null +++ b/documentations/docbook/src/getting_started.md @@ -0,0 +1,63 @@ +# Getting started + +Select the lastest available tag +```sh +git clone --depth 1 --branch git@github.com:Benncs/rcmtool.git +``` + + +## Rust + +To build the full cli application + +```sh +cargo build --release +``` + +Some example are available with : +```sh +cargo run --example +``` + +### VTK + +The VTK features can be enabled if VTK dev dependencies are installed in the system. +For rust build the feature 'use_vtk' is needed. +```sh +cargo build --release --features use_vtk +``` + +## Python package + +[uv](https://docs.astral.sh/uv/) package manager is highly recommended. + +To enable use of python package, the following + +```sh +uv venv +uv pip install -r pyproject.toml --extra examples +``` + + +Example can be run with +```sh +export EXAMPLE_ROOT='/path/to/cma/case' +uv run examples/python/mixing_simple.py +``` + +> **Note:** The `export` command works only on POSIX-compliant systems (Linux, macOS, Unix). +> Windows users should use `set` (Command Prompt) or `$env:` (PowerShell) instead. + + +### VTK + +The VTK features can be enabled if VTK dev dependencies are installed in the system. +Python with automatically detects + +## C++ + +Shared library object can be compiled with [meson](https://mesonbuild.com/) build system. +The best way is to use this folder as subproject and add the following to your 'meson.build' +```meson +cmtool = dependency('rcmtool', required: true, version: '>=0.1.0', static: true) +``` diff --git a/documentations/docbook/src/main.md b/documentations/docbook/src/main.md index 6bf56ff6..9c13f568 100644 --- a/documentations/docbook/src/main.md +++ b/documentations/docbook/src/main.md @@ -3,7 +3,7 @@ The *CMTool* project aims to develop a set of tool to perform simulation using a compartment modelling approach. Compartment modeling is an approach used to reduce computational cost in simulation while maintaining a balance between efficiency and precision. -The core part of the crate is focused on process and simplify computational fluid dynamics (CFD) results obtained from simulations on fine computational grids into coarser grids, known as compartments.Additonal features includes generation of Compartment Model drectly from models (namely Plug Flow Reactor Model) +The core part of the crate is focused on process and simplify computational fluid dynamics (CFD) results obtained from simulations on fine computational grids into coarser grids, known as compartments. Additonal features includes generation of Compartment Model drectly from models (namely Plug Flow Reactor Model) ## Objectives diff --git a/documentations/docbook/src/modeler.md b/documentations/docbook/src/modeler.md index 8b14d9c4..2293fabd 100644 --- a/documentations/docbook/src/modeler.md +++ b/documentations/docbook/src/modeler.md @@ -1,2 +1,19 @@ # Modeler +![CMA Philosophy](./assets/cm_assemble.svg "CMA Philosophy") + +**CMTool** has been designed to allow to reuse and assemble different compartment model. THis is either be to model from stach scale-down experiments or assemble CFD-based compartment model. The philosophy is to designed reactors with input/output position and flow for gas and liquid, assemble as desired in any direction. + +## Building Flowmaps with XML + +To assemble flowmaps almost seamlessly, compartment models are defined using an **XML file**. + +Each XML file must contain the following core elements: + +| Tag | Description | +|:----|:------------| +| **Reactors** | Defines individual reactor units. Different tags exist for **0D**, **1D**, **2D**, or pre-existing (**CFD-based**) reactors. | +| **Connections** | Specifies **intra-flowmap**, connections between reactors (liquid and/or gas phases).| +| **Feeds** | Defines input flows entering the domain from outside (liquid and/or gas) | + +More details [here](./tuto/basic_xml.md) diff --git a/documentations/docbook/src/refs.bib b/documentations/docbook/src/refs.bib new file mode 100644 index 00000000..be0629fc --- /dev/null +++ b/documentations/docbook/src/refs.bib @@ -0,0 +1,75 @@ + +@article{morchain_dynamic_2024, + title = {A dynamic compartment model for spatially heterogeneous reactors: {Scalar} and {Monte}-{Carlo} particle mixing}, + volume = {205}, + issn = {02638762}, + shorttitle = {A dynamic compartment model for spatially heterogeneous reactors}, + url = {https://linkinghub.elsevier.com/retrieve/pii/S026387622400217X}, + doi = {10.1016/j.cherd.2024.04.014}, + language = {en}, + urldate = {2024-10-09}, + journal = {Chemical Engineering Research and Design}, + author = {Morchain, Jérôme and Mayorga, Carlos and Villedieu, Philippe and Liné, Alain}, + month = may, + year = {2024}, + pages = {628--639}, +} + +@article{delafosse_eulerlagrange_2015, + title = {Euler–{Lagrange} approach to model heterogeneities in stirred tank bioreactors – {Comparison} to experimental flow characterization and particle tracking}, + volume = {134}, + issn = {00092509}, + url = {https://linkinghub.elsevier.com/retrieve/pii/S0009250915003851}, + doi = {10.1016/j.ces.2015.05.045}, + language = {en}, + urldate = {2024-09-18}, + journal = {Chemical Engineering Science}, + author = {Delafosse, Angélique and Calvo, Sébastien and Collignon, Marie-Laure and Delvigne, Frank and Crine, Michel and Toye, Dominique}, + month = sep, + year = {2015}, + pages = {457--466}, +} + +@article{hristov_simplified_2004, + title = {A {Simplified} {CFD} for {Three}-dimensional {Analysis} of {Fluid} {Mixing}, {Mass} {Transfer} and {Bioreaction} in a {Fermenter} {Equipped} with {Triple} {Novel} {Geometry} {Impellers}}, + volume = {82}, + copyright = {https://www.elsevier.com/tdm/userlicense/1.0/}, + issn = {09603085}, + url = {https://linkinghub.elsevier.com/retrieve/pii/S0960308504704034}, + doi = {10.1205/096030804322985281}, + language = {en}, + number = {1}, + urldate = {2024-09-16}, + journal = {Food and Bioproducts Processing}, + author = {Hristov, H.V. and Mann, R. and Lossev, V. and Vlaev, S.D.}, + month = mar, + year = {2004}, + pages = {21--34}, +} + +@article{delafosse_cfd-based_2014, + title = {{CFD}-based compartment model for description of mixing in bioreactors}, + volume = {106}, + issn = {00092509}, + url = {https://linkinghub.elsevier.com/retrieve/pii/S0009250913007690}, + doi = {10.1016/j.ces.2013.11.033}, + language = {en}, + urldate = {2025-12-10}, + journal = {Chemical Engineering Science}, + author = {Delafosse, Angélique and Collignon, Marie-Laure and Calvo, Sébastien and Delvigne, Frank and Crine, Michel and Thonart, Philippe and Toye, Dominique}, + month = mar, + year = {2014}, + pages = {76--85}, +} + +@book{computational_engineering_international_inc_ensight_2003, + address = {Apex, NC, USA}, + edition = {Version 7.6}, + title = {{EnSight} {User} {Manual}}, + url = {https://dav.lbl.gov/archive/NERSC/Software/ensight/doc/Manuals/UserManual.pdf}, + abstract = {Comprehensive user manual for EnSight visualization software, covering input/output, parts, variables, GUI, menus, features, modes, data formats, utility programs, and rendering techniques.}, + publisher = {Computational Engineering International, Inc.}, + author = {{Computational Engineering International, Inc.}}, + year = {2003}, + file = {PDF:/home-local/casale/Zotero/storage/FHCI7CI2/Computational Engineering International, Inc. - 2003 - EnSight User Manual.pdf:application/pdf}, +} diff --git a/documentations/docbook/src/tuto/basic_mixing.md b/documentations/docbook/src/tuto/basic_mixing.md index 922910fd..745e89e4 100644 --- a/documentations/docbook/src/tuto/basic_mixing.md +++ b/documentations/docbook/src/tuto/basic_mixing.md @@ -1,10 +1,11 @@ # Basic Mixing Example -This example demonstrates how to use Cmtool as a Compartmental Modeling (CM) framework to perform realistic simulations. We focus on reactor mixing, a fundamental application of compartmental modeling. +This example demonstrates how to use Cmtool as a Compartmental Modeling (CM) framework to perform realistic simulations. We focus on reactor mixing, a fundamental application of compartmental modeling. ## Motivation -Mixing is one of the simplest applications of Compartmental Modeling, involving only the flows between compartments. As established in previous research (TODO: add references), mixing does not alter the total system mass. + +Mixing is one of the simplest applications of Compartmental Modeling, involving only the flows between compartments, mixing does not alter the total system mass. Following example was established in previous research @@morchain_dynamic_2024, but with a totally different implementation. The rate of change of mass in each compartment can be described by the following differential equation: @@ -25,28 +26,34 @@ This formulation, allows for efficient computation and integration over time, ma The mixing experiment involves integrating this ordinary differential equation (ODE) over time. An important aspect is that the matrix F can be time-dependent, denoted as F(t). **CMTool** simplifies this process with the **FlowMapTransitioner** trait.This trait allows users to load their cases, whether transient (with multiple flow maps) or not. The flowmap transitioner (abbreviated *fmt* ) is responsible for handling transitions between different flow maps and will automatically update the flow map over time as needed. +## Results +![Obtained with python code](../assets/tuto_mixing.svg "Obtained with python code") ## Python Code -Our Python wrapper offers an easy-to-use interface for out-of-the-box simulations and proofs of concept. With compatibility with the numpy API, performing calculations becomes straightforward. Currently, one type of transitioner is available, and the volumes and transitions matrix can be easily accessed from the iterator. Compatibility with sparse matrices ensures efficient integrations. Furthermore, this setup is compatible with the widely-used `solve_ivp` function for performant ODE integration, although more sophisticated methods can be envisioned. +Our Python wrapper offers an easy-to-use interface for out-of-the-box simulations and proofs of concept. With compatibility with the **numpy** API, performing calculations becomes straightforward. Currently, one type of transitioner is available, and the volumes and transitions matrix can be easily accessed from the iterator. Compatibility with sparse matrices ensures efficient integrations. Furthermore, this setup is compatible with the widely-used `solve_ivp` function for performant ODE integration, although more sophisticated methods can be set up. ```python -{{#include ../../../../example/python/mixing_simple.py}} +{{#include ../../../../examples/python/mixing_simple.py}} ``` + + ## Rust Code -Here is an example of how to implement this in Rust. The **CMTool** components are compatible with popular linear algebra libraries like Nalgebra and Ndarray. Although the interface is currently low-level, it effectively demonstrates how to use CMtool for performing integrations. This example uses a first-order explicit scheme for simplicity. +Here is an example of how to implement this in Rust. The **CMTool** components are compatible with popular linear algebra libraries like **Nalgebra** and **Ndarray**. Although the interface is currently low-level, it effectively demonstrates how to use CMtool for performing integrations. This example uses a first-order explicit scheme for simplicity. -*Note*: For mixing problems, an implicit method is generally recommended for better performance and stability. Using Rust is advisable for building complete simulation tools. Rust allows developers to focus on writing performant code and provides more control over data, although it may require more development effort. +> *Note*: For mixing problems, an implicit method is generally recommended for better performance and stability. + + ```rust -{{#include ../../../../example/src/mixing_simple.rs}} +{{#include ../../../../examples/src/mixing_simple.rs}} ``` diff --git a/documentations/docbook/src/tuto/basic_xml.md b/documentations/docbook/src/tuto/basic_xml.md new file mode 100644 index 00000000..3655d8e6 --- /dev/null +++ b/documentations/docbook/src/tuto/basic_xml.md @@ -0,0 +1,48 @@ +# XML + +## How to use + +- Rust API: `cmtool-assemble` exposes `Parser` struct as well as `generate_domain` +- App: rcmtool: + ```sh + cargo run -- xml --help + ``` +- Python API: TBD + +- Example: + - Names + - case_0d1d + - simple_0d + - simple_1d + - neubauer + - Run with + ```sh + cargo run --example + ``` + + +For Rust and Python API, please refer to RustDoc [Here](TBD) + + +## XML details + +### XML tags + +- Reactors: + - Reactor0D: + - Volume fraction + - Diameter/Height or volume + - Reactor1D: + - + - ReactorFromFile + - Path to file +- Connections: + - flux +- Fleeds + - Flux + +Different simple examples [are available](../../../../examples/data) to assemble flowmaps and scalar fields such as this one : + +```xml +{{#include ../../../../examples/data/case_0d1d_liq/reactors.xml}} +``` diff --git a/documentations/index.html b/documentations/index.html index 6428509d..16cf5df6 100644 --- a/documentations/index.html +++ b/documentations/index.html @@ -47,7 +47,7 @@

Useful Resources

- © 2025 CMTool. All Rights Reserved. | Version 0.1.2 | + © 2025 CMTool. All Rights Reserved. | Version 0.1.4 |

diff --git a/documentations/roadmap.md b/documentations/roadmap.md new file mode 100644 index 00000000..06ba1100 --- /dev/null +++ b/documentations/roadmap.md @@ -0,0 +1,17 @@ +# RCMTool Roadmap + +## Chore +- refractor cmtool-core/model to put cfd oriented into separated mod and move "reactor model" such as 0d/pfr from cmtool-assemble to cmtool-core +- + +## Core + +- CFD-to_CMA algo doesn't work because of bad interface area calculation + - Scalar field ok + - vector field direction seems to be good but magnitude false because of area + +## Assemble + +- Add autofeed tag in xml datamodel + - Auto calculate flowrate from given dilution rate + - If cfd-based reactor select different source (N) to spread flowrate (Q/N) diff --git a/example/Cargo.toml b/examples/Cargo.toml similarity index 54% rename from example/Cargo.toml rename to examples/Cargo.toml index 9d68c76a..2dc5bdc6 100644 --- a/example/Cargo.toml +++ b/examples/Cargo.toml @@ -13,7 +13,27 @@ cmtool-data.workspace = true nalgebra.workspace = true nalgebra-sparse.workspace=true ndarray.workspace=true - +cmtool-assemble.workspace = true [[example]] path = "src/mixing_simple.rs" name = "mixing_simple" + + + +[[example]] +name = "case_0d1d" +path = "src/map_generation/case_0d1d.rs" + +[[example]] +name = "simple_0d" +path = "src/map_generation/simple_0d.rs" + +[[example]] +name = "simple_1d" +path = "src/map_generation/simple_1d.rs" + +[[example]] +name = "neubauer" +path = "src/map_generation/neubauer.rs" + + diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..851b8fd5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +# Case Example + +## Reactor and Connection Template + +- **example_1**: Script with an XML example for the full configuration of multiple connected reactors. diff --git a/examples/data/case_0d1d/reactors.xml b/examples/data/case_0d1d/reactors.xml new file mode 100644 index 00000000..e245b58c --- /dev/null +++ b/examples/data/case_0d1d/reactors.xml @@ -0,0 +1,72 @@ + + + + + + + 0.01 + 3 + + + 0.2 + 100 + 9e-9 + + + + 10 + + 0.31 + + + + + + + 1e-6 + + + + + 1e-6 + + + + + + 1e-5 + + + + + 1e-5 + + + + + + + + 2e-5 + + + + + + 6e-5 + + + + + + 3e-8 + + + + + + 6e-8 + + + + diff --git a/examples/data/case_0d1d_liq/reactors.xml b/examples/data/case_0d1d_liq/reactors.xml new file mode 100644 index 00000000..284e4eb8 --- /dev/null +++ b/examples/data/case_0d1d_liq/reactors.xml @@ -0,0 +1,46 @@ + + + + + + + 0.01 + 3 + + + 0. + 100 + 9e-9 + + + + 10 + + 0. + + + + + + + 1e-6 + + + + + 1e-6 + + + + + + + 3e-8 + + + + + 6e-8 + + + diff --git a/examples/data/neubauer/reactors.xml b/examples/data/neubauer/reactors.xml new file mode 100644 index 00000000..d6b44799 --- /dev/null +++ b/examples/data/neubauer/reactors.xml @@ -0,0 +1,47 @@ + + + + + + + 0.0192 + 3 + + + 0.2 + 100 + 0 + + + + + 9.305e-3 + + 0.37 + + + + + + + 6.1e-6 + + + + + 6.1e-6 + + + + + + + 2.33333333333333E-05 + + + + + 6.66666666666667E-05 + + + \ No newline at end of file diff --git a/examples/data/neubauer/reactors_batch.xml b/examples/data/neubauer/reactors_batch.xml new file mode 100644 index 00000000..38a0f8fd --- /dev/null +++ b/examples/data/neubauer/reactors_batch.xml @@ -0,0 +1,45 @@ + + + + + + + + 0.0192 + 3 + + + 0.2 + 100 + 0 + + + + 9.305e-3 + + 0.37 + + + + + + + + 2.33333333333333E-05 + + + + + 6.1e-6 + + + + + + + + 6.66666666666667E-05 + + + + diff --git a/examples/data/simple_0d/reactors.xml b/examples/data/simple_0d/reactors.xml new file mode 100644 index 00000000..b59faf57 --- /dev/null +++ b/examples/data/simple_0d/reactors.xml @@ -0,0 +1,15 @@ + + + + + + + 10.0 + + + + 0.0 + + + + diff --git a/examples/data/simple_0d1d/reactors.xml b/examples/data/simple_0d1d/reactors.xml new file mode 100644 index 00000000..588f211d --- /dev/null +++ b/examples/data/simple_0d1d/reactors.xml @@ -0,0 +1,48 @@ + + + + + + + 0.01 + 1 + + + 0.2 + 66 + 0 + + + + 10 + + 0.1 + + + + + + + + + 6.1e-6 + + + + + 6.1e-6 + + + + + + 4.1e-6 + + + + + 4.1e-6 + + + + diff --git a/examples/data/simple_1d/reactors.xml b/examples/data/simple_1d/reactors.xml new file mode 100644 index 00000000..8a57a75f --- /dev/null +++ b/examples/data/simple_1d/reactors.xml @@ -0,0 +1,28 @@ + + + + + + + 0.01 + 1 + + + 0.2 + 50 + 1e-9 + + + + + + + 2.33333333333333E-05 + + + + + 4.33333333333333E-05 + + + diff --git a/examples/python/complete_reactive.py b/examples/python/complete_reactive.py new file mode 100644 index 00000000..f70b7056 --- /dev/null +++ b/examples/python/complete_reactive.py @@ -0,0 +1,209 @@ +""" +Simple Example Script: Reactive Simulation with pycmtool +====================================================== + +**Purpose:** +This script demonstrates how to use the `pycmtool` library to perform a simple mixing simulation. +It integrates a mass balance over time using a sparse matrix representation of the system +The core functionality is used for chemical or compartmental mixing simulations. + +**Key Features:** +- Uses `pycmtool` to construct flow sparse matrices from system states. +- Integrates mass balance equations using `scipy.integrate.solve_ivp`. +- Visualizes results with `matplotlib`. + +**Example Workflow:** +1. Define initial mass distribution. +2. Use `pycmtool` to advance the system state and construct sparse matrices. +3. Integrate the system over a specified duration. +4. Plot the results. + +**Author:** CASALE Benjamin +**Date:** 2025-12-08 +**Version:** 1.0 +""" + +import os +from typing import Callable + +import matplotlib.pyplot as plt +import numpy as np +import pycmtool +from scipy.integrate import solve_ivp + + +def integration( + two_phase_flow: bool, + fmt: "pycmtool.Transitioner", + mass_0: np.ndarray, + duration: float, + n_species, + f_reaction: Callable, +): + """ + Perform a mixing simulation by integrating the mass balance ODE over a specified duration. + + This function uses `pycmtool` to advance the system state and construct sparse matrices, + then integrates the mass balance equations using `scipy.integrate.solve_ivp`. + + Args: + fmt: A `pycmtool.Transitioner` object to advance the system state. + mass_0: Initial mass distribution (1D array of shape `(n_species * n_compartments,)`). + duration: Total simulation time (in seconds or any consistent time unit). + n_species: Number of species in the simulation . + """ + n_p = 2 if two_phase_flow else 1 + + def phase_dt(_mass, iphase, hydro_state, n_v): + transition = pycmtool.get_sparse_transition_matrix(hydro_state) + vol = hydro_state.volumes + # C = _mass/vol + # print(C.shape) + # C = np.zeros((dims[0],dims[1],dims[-1])) + C = _mass[:, :, iphase, :] / vol[:, None] + mm = np.zeros_like(C) + for i in range(n_v): + mm[:, :, i] = C[:, :, i] @ transition + return C, vol, mm + + def wrap_tpf(t: float, x: np.ndarray) -> np.ndarray: + """Wrapper function for ODE integration.""" + d_t = 0 # Not used for this transitioner + it = fmt.advance(t, d_t) + n_c = it.n_compartments + _mass = x.reshape((n_species, n_c, n_p, -1)) + n_v = _mass.shape[-1] + + C_l, vl, lflows = phase_dt(_mass, 0, it.liquid, n_v) + Cg, vg, gflows = phase_dt(_mass, 1, it.gas, n_v) + + R_l = f_reaction(C_l) * vl[:, np.newaxis] + + T = np.zeros_like(R_l) + T[2, :, :] = 0.2 * (0.03 * Cg[2, :, :] - C_l[2, :, :]) * vl[:, np.newaxis] + + dmdt = np.zeros_like(_mass) + dmdt[:, :, 0, :] = lflows + R_l + T + dmdt[:, :, 1, :] = gflows - T + return dmdt.reshape(-1, n_v) + + def wrap_l(t: float, x: np.ndarray) -> np.ndarray: + """Wrapper function for ODE integration.""" + d_t = 0 # Not used for this transitioner + it = fmt.advance(t, d_t) + n_c = it.n_compartments + _mass = x.reshape((n_species, n_c, n_p, -1)) + n_v = _mass.shape[-1] + C_l, vl, lflows = phase_dt(_mass, 0, it.liquid, n_v) + R_l = f_reaction(C_l) * vl[:, np.newaxis] + return (lflows + R_l).reshape(-1, n_v) + + wrap = wrap_tpf if two_phase_flow else wrap_l + return solve_ivp( + wrap, + (0, duration), + mass_0.reshape(-1), + method="BDF", + vectorized=True, + ) + + +def initial_c_distribution(n_c, n_p): + """ + Generate a random initial concentration distribution + for a simulation with `n_c` compartments. + """ + n = np.random.random((n_c, n_p)) + return n + + +def gm0(two_phase_flow: bool, C, it): + if two_phase_flow: + m = np.zeros_like(C) + m[:, :, 0] = C[:, :, 0] * it.liquid.volumes + m[:, :, 1] = C[:, :, 1] * it.gas.volumes + return m + else: + return C[:, :, 0] * it.liquid.volumes + + +def check_mixing(fmt, n_s, final_time: float, reaction_rate): + it = fmt.get_current() + n_c = it.n_compartments + + two_phase_flow = it.has_gas() + n_p = 2 if two_phase_flow else 1 + C = np.zeros((n_s, n_c, n_p)) + + C[0, :, :] = 0.7 * initial_c_distribution(n_c, n_p) + C[1, :, :] = 0.2 * initial_c_distribution(n_c, n_p) + C[2, :, 0] = 9e-3 + if two_phase_flow: + C[2, :, 1] = 300e-3 + m0 = gm0(two_phase_flow, C, it) + + sol = integration(two_phase_flow, fmt, m0, final_time, n_s, reaction_rate) + y = sol.y.reshape((n_s, n_c, n_p, -1)) + it = fmt.get_current() + plt.figure() + + plt.plot(y[0, :, 0, 0] / it.liquid.volumes, label="glucose") + plt.plot(y[1, :, 0, 0] / it.liquid.volumes, "--") + plt.title("concentration in all compartments init") + plt.legend() + + it = fmt.get_at(fmt.n_flowmaps - 1) + + plt.figure() + plt.plot(y[0, :, 0, -1] / it.liquid.volumes, label="glucose") + plt.plot(y[1, :, 0, -1] / it.liquid.volumes, "--") + plt.title("concentration in all compartments final") + plt.legend() + + plt.figure() + plt.plot(sol.t, y[0, n_c // 3, 0, :] / it.liquid.volumes[n_c // 3], label="glucose") + plt.plot(sol.t, y[1, n_c // 3, 0, :] / it.liquid.volumes[n_c // 3], "--") + plt.title("concentration = f(t)") + plt.legend() + + plt.figure() + plt.plot(y[2, n_c // 3, 0, :] / it.liquid.volumes[n_c // 3]) + # plt.plot(y[2, n_c//3,1,:]/it.gas.volumes[n_c//3],'--') + plt.legend() + + # plt.figure() + # plt.plot(y[2, :, 0, -1] / it.liquid.volumes) + # plt.plot(y[2, :, 1, -1] / it.liquid.volumes, "--") + # plt.title("concentration in all compartments final2") + # plt.legend() + + plt.show() + + +if __name__ == "__main__": + final_time = 15 * 3600 + root = os.environ["EXAMPLE_ROOT"] + + N_SPECIES = 3 + + def _reaction_rate(Cl): + mum = 0.8 / 3600 + K_S = 0.1 + r = np.zeros_like(Cl) + mu = ( + mum * Cl[0, :, :] / (Cl[0, :, :] + K_S) * Cl[2, :, :] / (Cl[2, :, :] + 1e-6) + ) + dx = Cl[1, :, :] * mu + r[0, :, :] = -dx + r[1, :, :] = dx / 2 + r[2, :, :] = -dx / 20 + # for i in range(Cl.shape[-1]): + # mu = mum * Cl[0, :,i] / (Cl[0, :,i] + K_S) + # dx = Cl[1, :,i] * mu + # r[0,:,i]=-dx + # r[1,:,i]=dx/2 + return r + + # Let CMTool read and load the full case automatically, ready to iterate + fmt = pycmtool.data.get_transitioner(root) + check_mixing(fmt, N_SPECIES, final_time, _reaction_rate) diff --git a/examples/python/generate_reference.py b/examples/python/generate_reference.py new file mode 100644 index 00000000..2f94f1a8 --- /dev/null +++ b/examples/python/generate_reference.py @@ -0,0 +1,56 @@ +import numpy as np +import pycmtool + +if __name__ == "__main__": + NX, NY, NZ = 3, 3, 2 + n_zone = NX * NY * NZ + + vol_tot = 10.0 # L + + def zone_weight(x, y, z): + bx = x == 0 or x == NX - 1 + by = y == 0 or y == NY - 1 + bz = z == 0 or z == NZ - 1 + boundary_count = bx + by + bz + return {3: 0.5, 2: 0.8, 1: 1.2, 0: 2.0}[boundary_count] + + def zone_id(x, y, z): + return x + NX * y + NX * NY * z + + weights = np.array( + [zone_weight(x, y, z) for z in range(NZ) for y in range(NY) for x in range(NX)] + ) + # vol = vol_tot * weights / weights.sum() + vol = vol_tot / n_zone * np.ones((n_zone)) + vol_array = pycmtool.data.scalar_from_data(vol) + + Q_xy = 0.5 + Q_z = 0.3 + + flow_map = [] + + for z in range(NZ): + for y in range(NY): + for x in range(NX): + src = zone_id(x, y, z) + if x + 1 < NX: + tgt = zone_id(x + 1, y, z) + flow_map.append(pycmtool.data.new_raw_flux(src, tgt, Q_xy, Q_xy)) + if y + 1 < NY: + tgt = zone_id(x, y + 1, z) + flow_map.append(pycmtool.data.new_raw_flux(src, tgt, Q_xy, Q_xy)) + if z + 1 < NZ: + tgt = zone_id(x, y, z + 1) + flow_map.append(pycmtool.data.new_raw_flux(src, tgt, Q_z, Q_z)) + + fm = pycmtool.data.vector_from_data(n_zone, flow_map) + + fm.write("/tmp/fm/flowL.raw") + vol_array.write("/tmp/fm/vofL.raw") + + print(vol_array.data) + + print(fm.n_zone, vol_array.n_zone) + + v = pycmtool.data.read_flowmap("/tmp/fm/flowL.raw", "/tmp/fm/vofL.raw") + print(v.flowmap) diff --git a/example/python/mixing_simple.py b/examples/python/mixing_simple.py similarity index 82% rename from example/python/mixing_simple.py rename to examples/python/mixing_simple.py index cee541c6..f25c0367 100644 --- a/example/python/mixing_simple.py +++ b/examples/python/mixing_simple.py @@ -54,10 +54,12 @@ def wrap(t: float, x: np.ndarray) -> np.ndarray: d_t = 0 # Not used for this transitioner # Advance iterator to the corresponding flowmap (according to t or dt) it = fmt.advance(t, d_t) + n_c = it.n_compartments + liquid_state = it.liquid # Get the transition matrix - transition = pycmtool.get_sparse_transition_matrix(it) - n_c = transition.shape[0] # Number of compartments - vol = it.volumes # Volume of each compartment + transition = pycmtool.get_sparse_transition_matrix(liquid_state) + + vol = liquid_state.volumes # Volume of each compartment _mass = x.reshape((n_species, n_c)) # Reshape to (N_SPECIES, n_compartments) C = _mass / vol # Concentration: mass / volume return (C @ transition).reshape(-1) # Return flattened array for ODE solver @@ -75,10 +77,10 @@ def initial_c_distribution(n_c): """ Generate a random initial concentration distribution for a simulation with `n_c` compartments. """ - # return np.random.random((1, n_c)) - m = np.zeros((n_c,)) - m[0] = 1 - return m + return np.random.random((1, n_c)) + # m = np.zeros((n_c,)) + # m[0] = 1 + # return m def get_normalized(it, y, i): @@ -88,19 +90,18 @@ def get_normalized(it, y, i): The normalization is performed by dividing each compartment's concentration by the mean concentration of the species across all compartments. This is useful for comparing relative concentrations. """ - vol = it.volumes + vol = it.liquid.volumes m0_c = y[0, :, i] return (m0_c / vol) / np.mean(m0_c / vol, axis=0) def check_mixing(fmt, final_time: float): it = fmt.get_current() - transition = pycmtool.get_sparse_transition_matrix(it) - n_c = transition.shape[0] + n_c = it.n_compartments C = np.zeros((N_SPECIES, n_c)) C[0, :] = initial_c_distribution(n_c) - vol = it.volumes + vol = it.liquid.volumes m0 = C * vol sol = integration(fmt, m0, final_time, N_SPECIES) @@ -114,6 +115,7 @@ def check_mixing(fmt, final_time: float): m0m = np.sum(m0_c, axis=0) mfm = np.sum(mt_c, axis=0) + print("Total volume: ", np.sum(vol)) print("Inital mass: ", m0m) print("Final mass: ", mfm) print("Initial normalized C: ", c_init[:5]) @@ -121,15 +123,25 @@ def check_mixing(fmt, final_time: float): print("Final variance: ", np.var(c_final, axis=0)) plt.figure() - plt.plot(c_final) - plt.title("Normalized concentration in all compartments") + plt.style.use("tableau-colorblind10") + plt.plot(c_final, label="final") + plt.plot(c_init, "x", markersize=2.5, label="init") + plt.title("Normalized concentration in all compartments (1 is the target value)") + plt.xlabel("Compartment ID") + plt.ylabel("Normalized concentration") plt.legend() + + plt.figure() + plt.style.use("tableau-colorblind10") + plt.plot(sol.t, y[0, 0, :]) + plt.legend() + plt.show() assert abs(m0m - mfm) < 1e-8 if __name__ == "__main__": - final_time = 500 + final_time = 200 root = os.environ["EXAMPLE_ROOT"] # Let CMTool read and load the full case automatically, ready to iterate diff --git a/examples/python/reactive_mixing.py b/examples/python/reactive_mixing.py new file mode 100644 index 00000000..d6b0fedd --- /dev/null +++ b/examples/python/reactive_mixing.py @@ -0,0 +1,157 @@ +""" +Simple Example Script: Reactive and Mixing Simulation with pycmtool +====================================================== + +**Purpose:** +This script demonstrates how to use the `pycmtool` library to perform a simple mixing simulation. +It integrates a mass balance over time using a sparse matrix representation of the system +The core functionality is used for chemical or compartmental mixing simulations. + +**Key Features:** +- Uses `pycmtool` to construct flow sparse matrices from system states. +- Integrates mass balance equations using `scipy.integrate.solve_ivp`. +- Visualizes results with `matplotlib`. + +**Example Workflow:** +1. Define initial mass distribution. +2. Use `pycmtool` to advance the system state and construct sparse matrices. +3. Integrate the system over a specified duration. +4. Plot the results. + +**Author:** CASALE Benjamin +**Date:** 2025-11-24 +**Version:** 1.0 +""" + +import os + +import matplotlib.pyplot as plt +import numpy as np +import pycmtool +from scipy.integrate import solve_ivp + +##### MONOD.py +N_SPECIES = 2 # We use 2 species to demonstrates that pycmtool can handle different dissolved species + +K = 1e-2 +mu = 0.8 / 3600 +K_S = 0.1 + + +def monod(C): + return mu * C[0, :] / (C[0, :] + K_S) + + +##### + + +def integration( + fmt: "pycmtool.Transitioner", mass_0: np.ndarray, duration: float, n_species +): + """ + Perform a mixing simulation by integrating the mass balance ODE over a specified duration. + + This function uses `pycmtool` to advance the system state and construct sparse matrices, + then integrates the mass balance equations using `scipy.integrate.solve_ivp`. + + Args: + fmt: A `pycmtool.Transitioner` object to advance the system state. + mass_0: Initial mass distribution (1D array of shape `(n_species * n_compartments,)`). + duration: Total simulation time (in seconds or any consistent time unit). + n_species: Number of species in the simulation . + """ + f = monod + + def wrap(t: float, x: np.ndarray, f) -> np.ndarray: + """Wrapper function for ODE integration.""" + d_t = 0 # Not used for this transitioner + # Advance iterator to the corresponding flowmap (according to t or dt) + it = fmt.advance(t, d_t) + liq_state = it.liquid + # Get the transition matrix + transition = pycmtool.get_sparse_transition_matrix(liq_state) + n_c = transition.shape[0] # Number of compartments + vol = liq_state.volumes # Volume of each compartment + _mass = x.reshape((n_species, n_c)) # Reshape to (N_SPECIES, n_compartments) + C = _mass / vol # Concentration: mass / volume + R = np.zeros_like(C) + r = f(C) * vol + R[0, :] = -r + R[1, :] = r + return (C @ transition + R).reshape(-1) # Return flattened array for ODE solver + + w = lambda t, x: wrap(t, x, f) + + return solve_ivp( + w, + (0, duration), + mass_0.reshape(-1), + method="BDF", + vectorized=False, + ) + + +def initial_c_distribution(n_c): + """ + Generate a random initial concentration distribution for a simulation with `n_c` compartments. + """ + return np.random.random((1, n_c)) + # m = np.zeros((n_c,)) + # m[0] = 1 + # return m + + +def get_normalized(state, y, i): + """ + Calculate the normalized concentration for a given species across all compartments. + + The normalization is performed by dividing each compartment's concentration by the mean concentration + of the species across all compartments. This is useful for comparing relative concentrations. + """ + vol = state.volumes + m0_c = y[0, :, i] + return (m0_c / vol) / np.mean(m0_c / vol, axis=0) + + +def check_mixing(fmt, final_time: float): + it = fmt.get_current() + liq_state = it.liquid + transition = pycmtool.get_sparse_transition_matrix(liq_state) + n_c = transition.shape[0] + + C = np.zeros((N_SPECIES, n_c)) + C[0, :] = initial_c_distribution(n_c) + vol = liq_state.volumes + m0 = C * vol + + sol = integration(fmt, m0, final_time, N_SPECIES) + y = sol.y.reshape((N_SPECIES, n_c, -1)) + it = fmt.get_current() + m0_c = y[0, :, 0] + mt_c = y[0, :, -1] + c_init = get_normalized(fmt.get_at(0).liquid, y, 0) + c_final = get_normalized(fmt.get_at(fmt.n_flowmaps - 1).liquid, y, -1) + + m0m = np.sum(m0_c, axis=0) + mfm = np.sum(mt_c, axis=0) + + # print("Inital mass: ", m0m) + # print("Final mass: ", mfm) + # print("Initial normalized C: ", c_init[:5]) + # print("Final normalized C: ", c_final[:5]) + # print("Final variance: ", np.var(c_final, axis=0)) + plt.figure() + plt.plot(sol.t, y[0, 0, :], label="0") + plt.plot(sol.t, y[1, 0, :], label="1") + plt.title("Concentration in compartment0") + plt.legend() + plt.show() + + +if __name__ == "__main__": + final_time = 100 + root = os.environ["EXAMPLE_ROOT"] + # Let CMTool read and load the full case automatically, ready to iterate + + fmt = pycmtool.data.get_transitioner(root) + check_mixing(fmt, final_time) diff --git a/examples/python/simple_cm.py b/examples/python/simple_cm.py new file mode 100644 index 00000000..ffaec3c1 --- /dev/null +++ b/examples/python/simple_cm.py @@ -0,0 +1,35 @@ +import numpy as np +import pycmtool + + +def generate_liq_volume(): + total_volume = pycmtool.data.read_rawscalar("./out/RESULTS/vtot.raw") + vgas = pycmtool.data.read_rawscalar("./out/RESULTS/gas_vof.raw") + v_liq = np.array(total_volume.data - vgas.data) + sc = pycmtool.data.scalar_from_data(v_liq) + sc.write("./out/RESULTS/liq_vof.raw") + + +if __name__ == "__main__": + generate_liq_volume() + # Check volumes + total_volume = pycmtool.data.read_rawscalar("./out/RESULTS/vtot.raw") + vgas = pycmtool.data.read_rawscalar("./out/RESULTS/gas_vof.raw") + vliq = pycmtool.data.read_rawscalar("./out/RESULTS/liq_vof.raw") + + geometric_volume = np.pi * np.power(5.78824, 2) / 4 * 10.4736 + + vtot = np.sum(vliq.data) + np.sum(vgas.data) + print( + geometric_volume, + np.sum(vliq.data), + np.sum(vgas.data), + vtot, + np.sum(total_volume.data), + ) + assert np.abs(vtot - np.sum(total_volume.data)) < 1e-10 + + vL = pycmtool.data.read_rawscalar("/tmp/sanofi/vofL.raw") + vG = pycmtool.data.read_rawscalar("/tmp/sanofi/vofG.raw") + vtot = np.sum(vL.data) + np.sum(vG.data) + print(vtot, np.sum(vL.data), np.sum(vG.data)) diff --git a/examples/python/simple_export.py b/examples/python/simple_export.py new file mode 100644 index 00000000..05602dc0 --- /dev/null +++ b/examples/python/simple_export.py @@ -0,0 +1,19 @@ +import pycmtool +import pycmtool.export_vtk + +if __name__ == "__main__": + vx = "out/cuve_sldmsh_initmrf/x_velocity.raw" + vy = "out/cuve_sldmsh_initmrf/y_velocity.raw" + vz = "out/cuve_sldmsh_initmrf/z_velocity.raw" + scvx = pycmtool.data.read_rawscalar(vx) + scvy = pycmtool.data.read_rawscalar(vy) + scvz = pycmtool.data.read_rawscalar(vz) + + scvx_vtk = pycmtool.export_vtk.mk_scalar(scvz.data, "v_x") + scvy_vtk = pycmtool.export_vtk.mk_scalar(scvz.data, "v_y") + scvz_vtk = pycmtool.export_vtk.mk_scalar(scvz.data, "v_z") + + vtu_path = "out/cuve_sldmsh_initmrf/cma_case.vtu" + pycmtool.export_vtk.append_scalar( + vtu_path, "/tmp/test.vtu", scvx_vtk, scvx_vtk, scvy_vtk, scvz_vtk + ) diff --git a/examples/src/lib.rs b/examples/src/lib.rs new file mode 100644 index 00000000..9e08a8c9 --- /dev/null +++ b/examples/src/lib.rs @@ -0,0 +1 @@ +pub mod map_generation; diff --git a/examples/src/map_generation/case_0d1d.rs b/examples/src/map_generation/case_0d1d.rs new file mode 100644 index 00000000..0fd5af93 --- /dev/null +++ b/examples/src/map_generation/case_0d1d.rs @@ -0,0 +1,5 @@ +use cmtool_example::map_generation::generate; + +fn main() { + generate("examples/data/case_0d1d/reactors.xml"); +} diff --git a/examples/src/map_generation/mod.rs b/examples/src/map_generation/mod.rs new file mode 100644 index 00000000..2a66e64e --- /dev/null +++ b/examples/src/map_generation/mod.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; + +pub fn generate(reactor_input_file_name: &str) -> bool { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../out/examples/"); + if let Err(msg) = cmtool_assemble::headless_generate(reactor_input_file_name, path) { + eprintln!("{}", msg); + return false; + } + true +} + +#[cfg(test)] +mod domain_integration_test { + use super::*; + + fn common_test(name: &str) { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(format!("../examples/data/{}/reactors.xml", name)); + + assert!(generate(path.to_str().unwrap())); + } + + #[test] + fn case_0d1d() { + common_test("case_0d1d"); + } + #[test] + fn fcase_0d() { + common_test("simple_0d"); + } + #[test] + fn simple_0d1d() { + common_test("simple_0d1d"); + } + #[test] + fn case_0d1d_liq() { + common_test("case_0d1d_liq"); + } +} diff --git a/examples/src/map_generation/neubauer.rs b/examples/src/map_generation/neubauer.rs new file mode 100644 index 00000000..a45edc99 --- /dev/null +++ b/examples/src/map_generation/neubauer.rs @@ -0,0 +1,6 @@ +use cmtool_example::map_generation::generate; + +fn main() { + generate("examples/data/neubauer/reactors.xml"); + generate("examples/data/neubauer/reactors_batch.xml"); +} diff --git a/examples/src/map_generation/simple_0d.rs b/examples/src/map_generation/simple_0d.rs new file mode 100644 index 00000000..b7e8f8e1 --- /dev/null +++ b/examples/src/map_generation/simple_0d.rs @@ -0,0 +1,5 @@ +use cmtool_example::map_generation::generate; + +fn main() { + generate("examples/data/simple_0d/reactors.xml"); +} diff --git a/examples/src/map_generation/simple_1d.rs b/examples/src/map_generation/simple_1d.rs new file mode 100644 index 00000000..336ec065 --- /dev/null +++ b/examples/src/map_generation/simple_1d.rs @@ -0,0 +1,5 @@ +use cmtool_example::map_generation::generate; + +fn main() { + generate("examples/data/simple_1d/reactors.xml"); +} diff --git a/example/src/mixing_simple.rs b/examples/src/mixing_simple.rs similarity index 98% rename from example/src/mixing_simple.rs rename to examples/src/mixing_simple.rs index 8a4b823d..b07de52a 100644 --- a/example/src/mixing_simple.rs +++ b/examples/src/mixing_simple.rs @@ -119,10 +119,9 @@ fn check_mixing(mut fm_t: T, final_time: f64, n_step: us fn main() { let final_time: f64 = 50.; let n_step: usize = 5000; - //All fonction use generic, specify iterator type here let root = std::env::var("EXAMPLE_ROOT").unwrap(); - + //All fonctions use generic, specify iterator type here let t: DiscontinuousTransitioner = get_transitioner(&root).unwrap(); check_mixing(t, final_time, n_step); } diff --git a/flake.nix b/flake.nix index f0b7f9fd..7d438fae 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,7 @@ overlays = [ fenix.overlays.default ]; pkgs = import nixpkgs { inherit system overlays; }; lib = pkgs.lib; - + craneLib = (crane.mkLib pkgs).overrideToolchain (p: p.fenix.stable.withComponents [ "cargo" "clippy" @@ -29,11 +29,10 @@ src = lib.fileset.toSource { root = unfilteredRoot; fileset = lib.fileset.unions [ - # Default files from crane (Rust and cargo files) + (lib.fileset.fileFilter (file: file.hasExt "xsd") unfilteredRoot) (craneLib.fileset.commonCargoSources unfilteredRoot) - # Also keep any VTK files, this is a dirty fix for tests which use our example vtk file - # TODO: VTK files should be excluded to avoid indexing of residual output files - (lib.fileset.fileFilter (file: file.hasExt "vtk") unfilteredRoot) + (unfilteredRoot + "/cmtool-data/test_data") + (unfilteredRoot + "/examples/data") ]; }; @@ -47,18 +46,18 @@ commonArgs = { inherit src; strictDeps = true; - nativeBuildInputs = with pkgs; [ pkg-config ]; buildInputs = commonBuildInputs ++ (if pkgs.stdenv.isLinux then linuxBuildInputs else []) ++ (if pkgs.stdenv.isDarwin then darwinBuildInputs else []); - # LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ - # pkgs.lib.makeLibraryPath ( commonBuildInputs - # ++ (if pkgs.stdenv.isLinux then linuxBuildInputs else []) - # ++ (if pkgs.stdenv.isDarwin then darwinBuildInputs else []) ) - # }"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath ( + commonBuildInputs + ++ (if pkgs.stdenv.isLinux then linuxBuildInputs else []) + ++ (if pkgs.stdenv.isDarwin then darwinBuildInputs else []) + ++ [ pkgs.stdenv.cc.cc.lib ] + ); }; cargoArtifacts = craneLib.buildDepsOnly commonArgs; @@ -105,14 +104,14 @@ } ); }; - + devShells.default = craneLib.devShell { checks = self.checks.${system}; - + packages = with pkgs; [ - cargo-nextest # faster tests + cargo-nextest samply # profiling - taplo # TOML formatting + ]; }; }); diff --git a/meson.build b/meson.build index 073afa0b..1ac9df78 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'rcmtool-cxx', 'cpp', - version: '0.1.0', + version: '0.1.4', meson_version: '>= 1.3.0', ) out_dir = meson.current_build_dir() + '/rust_target' diff --git a/pyproject.toml b/pyproject.toml index f80e6178..cb2a54bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,12 @@ dependencies = ["numpy>=2.3.0", "scipy>=1.15"] requires-python = ">=3.12" dynamic = ["version"] +[project.optional-dependencies] +examples = [ + "matplotlib", + "pyqt5", +] + [tool.maturin] python-source = "cmtool-python/" profile = "release" diff --git a/uv.lock b/uv.lock index baae651e..4ad2d777 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,249 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, +] + [[package]] name = "numpy" version = "2.3.4" @@ -65,6 +307,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, ] +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +] + [[package]] name = "pycmtool" source = { editable = "." } @@ -73,11 +393,91 @@ dependencies = [ { name = "scipy" }, ] +[package.optional-dependencies] +examples = [ + { name = "matplotlib" }, + { name = "pyqt5" }, +] + [package.metadata] requires-dist = [ + { name = "matplotlib", marker = "extra == 'examples'" }, { name = "numpy", specifier = ">=2.3.0" }, + { name = "pyqt5", marker = "extra == 'examples'" }, { name = "scipy", specifier = ">=1.15" }, ] +provides-extras = ["examples"] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyqt5" +version = "5.15.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5-qt5" }, + { name = "pyqt5-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, +] + +[[package]] +name = "pyqt5-qt5" +version = "5.15.18" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" }, +] + +[[package]] +name = "pyqt5-sip" +version = "12.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/31/5ef342de9faee0f3801088946ae103db9b9eaeba3d6a64fefd5ce74df244/pyqt5_sip-12.18.0.tar.gz", hash = "sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94", size = 104143, upload-time = "2026-01-13T15:53:19.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/61/6d78d702016ac23d2b97634a3b6a831c3f7735f0552a1c8b058db96005d1/pyqt5_sip-12.18.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b29e4cda24748e59e5bd1bdad4812091a86b4b5b08c38b7f781eb55a5166f2b7", size = 124614, upload-time = "2026-01-13T15:52:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/19/bf/8f3efa10ddd3e76c1253865340ab7c2960ef96681d732b1f666c77430612/pyqt5_sip-12.18.0-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:163c2bba5e637c2222ec17d82a2c5aa158184a191923eb7d137cf4cfa0399529", size = 339412, upload-time = "2026-01-13T15:53:00.563Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/f1bcf6729d01bae6729cd790b22fd579dbe34014e8be031e6f10c5b9b2aa/pyqt5_sip-12.18.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ead5e0a64ad852ac60797989d8444a6a5bd834768536b04a07b40b2937d922f6", size = 282376, upload-time = "2026-01-13T15:52:59.172Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b7/d84c764ac9f1366be561255ec9bd88ee224fefdbdb349aee250f3003f0ca/pyqt5_sip-12.18.0-cp312-cp312-win32.whl", hash = "sha256:993fe3ed9a62a92e770f32d5344e3df56c2cacf1471f01b7feaf04818a2df1c4", size = 49523, upload-time = "2026-01-13T15:53:03.068Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e7/ef87178d5afa5f63be38556dc0df8af89f9bf74f2555f4dab6824c0fd150/pyqt5_sip-12.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:9b689e02e400abd1ce0a30cd6eae8eceabcf1bbba0395cb5c86e64ba74351d68", size = 58001, upload-time = "2026-01-13T15:53:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/79/67/8d43d0fea10ff48ddecc8534aead8b855dc80df80653b8b1bf9e1f993063/pyqt5_sip-12.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9254e5dd7676b76503ba20edcc919e7ac4a97b6c70a6fb2f9dba9e13b4c60509", size = 124605, upload-time = "2026-01-13T15:53:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/b08bc8efeb49c50c6cdac11417dc2c8eaefcac2f0a6382eae7b26dc0f232/pyqt5_sip-12.18.0-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c969631ada7293a81e1012b2264a62d69a91995b517586489dfe24421b87b9af", size = 339918, upload-time = "2026-01-13T15:53:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/24f82437b2f073cf39296b7c731b6a8bc0f5207911fdd93841a0ea9abe42/pyqt5_sip-12.18.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d84ac384a63285132e67762c87681191c25e28a1df7560287ec3889d9eb223b5", size = 282088, upload-time = "2026-01-13T15:53:06.632Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/20d3924943df34361fae9c6a0489ae89d0b07571693245c61678d185e4a4/pyqt5_sip-12.18.0-cp313-cp313-win32.whl", hash = "sha256:95bba4670ecf5cba73958b85aa2087c17838a402ed251c38e68060c7665c998b", size = 49501, upload-time = "2026-01-13T15:53:11.159Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/e251623c12968730730512a9e5150430e36246afbe64894610190b896f61/pyqt5_sip-12.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:aac4adc37df2f2ac1dc259409be1900f07332d140a12c9db7c84112cef64ff59", size = 58076, upload-time = "2026-01-13T15:53:09.928Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/b46a0116b1aacbb6156b2957eb5cb928c94b49f4626eb2540ca8d16ee757/pyqt5_sip-12.18.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8372ec8704bfd5e09942d0d055a1657eb4f702f4b30847a5e59df0496f99d67f", size = 124594, upload-time = "2026-01-13T15:53:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/58/63/df3037f11391c25c5b0ab233d22e58b8f056cb1ce16d7ecadb844421ce75/pyqt5_sip-12.18.0-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdb45c7cd2af7eccd7370b994d432bfc7965079f845392760724f26771bb59dc", size = 339056, upload-time = "2026-01-13T15:53:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/4f96b84520b8f8b7502682fd43f68f63ca6572b5858f56e5f61c76a54fe2/pyqt5_sip-12.18.0-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:92abe984becbde768954d6d0951f56d80a9868d2fd9e738e61fc944f0ff83dd6", size = 282439, upload-time = "2026-01-13T15:53:14.856Z" }, + { url = "https://files.pythonhosted.org/packages/79/8e/ccdf20d373ceba83e1d1b7f818505c375208ffde4a96376dc7dbe592406c/pyqt5_sip-12.18.0-cp314-cp314-win32.whl", hash = "sha256:bd9e3c6f81346f1b08d6db02305cdee20c009b43aa083d44ee2de47a7da0e123", size = 50713, upload-time = "2026-01-13T15:53:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/8486ed45977be615ec5371b24b47298b1cb0e1a455b419eddd0215078dba/pyqt5_sip-12.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:6d948f1be619c645cd3bda54952bfdc1aef7c79242dccea6a6858748e61114b9", size = 59622, upload-time = "2026-01-13T15:53:17.714Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] [[package]] name = "scipy" @@ -139,3 +539,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, ] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +]