diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f77ae97..4290ae79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,34 @@ # CHANGELOG + +## 0.1.5 +*Date: 06/2026 + +#### Features: + +- Deferred filesystem IO when assembling flowmaps + +- Generation contract + + +#### General Enhancements + +- Cleaner way to generate reactor in flowmap +- Reduce use of plain str for path handling, use Path/PathBuf instead +- Better error message + +#### Breaking Changes +- API endpoint (parser,generate_domain,...) change signature + +#### Bug Fixes + +#### Known Issue + +- CFD generated model does not compute flows correctly + + ## 0.1.4 -*Date: * +*Date: 04/2026 #### Features: diff --git a/Cargo.lock b/Cargo.lock index b81d8a94..3c53abe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -121,9 +121,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bytemuck" @@ -139,9 +139,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.57" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "shlex", @@ -155,9 +155,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -177,9 +177,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -195,7 +195,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmtool" -version = "0.1.4" +version = "0.1.5" dependencies = [ "clap", "cmtool-assemble", @@ -208,7 +208,7 @@ dependencies = [ [[package]] name = "cmtool-assemble" -version = "0.1.4" +version = "0.1.5" dependencies = [ "cmtool-data", "ndarray", @@ -222,7 +222,7 @@ dependencies = [ [[package]] name = "cmtool-core" -version = "0.1.4" +version = "0.1.5" dependencies = [ "cmtool-data", "enum_dispatch", @@ -232,7 +232,7 @@ dependencies = [ [[package]] name = "cmtool-cxx" -version = "0.1.4" +version = "0.1.5" dependencies = [ "cmtool-data", "cxx", @@ -243,7 +243,7 @@ dependencies = [ [[package]] name = "cmtool-data" -version = "0.1.4" +version = "0.1.5" dependencies = [ "enum_dispatch", "nalgebra", @@ -256,7 +256,7 @@ dependencies = [ [[package]] name = "cmtool-example" -version = "0.1.4" +version = "0.1.5" dependencies = [ "cmtool-assemble", "cmtool-data", @@ -267,7 +267,7 @@ dependencies = [ [[package]] name = "cmtool-python" -version = "0.1.4" +version = "0.1.5" dependencies = [ "cmtool-data", "nalgebra", @@ -365,9 +365,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -528,11 +528,23 @@ version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +[[package]] +name = "glam" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7" + +[[package]] +name = "glam" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" + [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -542,12 +554,13 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -555,9 +568,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -568,9 +581,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -582,15 +595,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -602,15 +615,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -634,9 +647,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -644,9 +657,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -660,9 +673,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "lazy_static" @@ -672,9 +685,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "link-cplusplus" @@ -687,9 +700,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -706,14 +719,14 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" dependencies = [ - "log 0.4.29", + "log 0.4.30", ] [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lz4_flex" @@ -747,9 +760,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "miniz_oxide" @@ -763,9 +776,9 @@ dependencies = [ [[package]] name = "nalgebra" -version = "0.34.1" +version = "0.34.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4d5b3eff5cd580f93da45e64715e8c20a3996342f1e466599cf7a267a0c2f5f" +checksum = "df76ea0ff5c7e6b88689085804d6132ded0ddb9de5ca5b8aeb9eeadc0508a70a" dependencies = [ "approx", "glam 0.14.0", @@ -784,6 +797,8 @@ dependencies = [ "glam 0.28.0", "glam 0.29.3", "glam 0.30.10", + "glam 0.31.1", + "glam 0.32.1", "matrixmultiply", "nalgebra-macros", "num-complex", @@ -968,9 +983,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -980,18 +995,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1007,9 +1022,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ "libc", "once_cell", @@ -1021,18 +1036,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -1040,9 +1055,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1052,9 +1067,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", @@ -1158,9 +1173,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "safe_arch" @@ -1205,7 +1220,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ - "log 0.4.29", + "log 0.4.30", "serde 1.0.228", "thiserror", "xml", @@ -1233,9 +1248,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -1256,9 +1271,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simba" @@ -1275,9 +1290,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "smallvec" @@ -1356,9 +1371,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -1409,9 +1424,9 @@ checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -1457,14 +1472,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vtkio" -version = "0.7.0-rc1" -source = "git+https://github.com/elrnv/vtkio.git#942ce7e27f172cb3c5a04ec6179dd7f0417d1e82" +version = "0.7.0-rc2" +source = "git+https://github.com/elrnv/vtkio.git#e66335b03ac6cfee34f34a7983bc6ece522ccab8" dependencies = [ "base64", "bytemuck", "byteorder", "flate2", - "log 0.4.29", + "log 0.4.30", "lz4_flex", "nom", "num-derive", @@ -1511,21 +1526,21 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xml" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "xsd-parser" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf636856509b72e6ce6d84232fdf0a6fb48ce9a04767bf6dc4f7292fa852940e" +checksum = "7ef11cbaf49b451df4fcac1991609a53f5f3ac351ea39598e4f437e908153eea" dependencies = [ "Inflector", "anyhow", @@ -1548,9 +1563,9 @@ dependencies = [ [[package]] name = "xsd-parser-types" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6c01d5f57551a048047e9f28ed5952e3ceab84882832e4b11a748c09122a4a" +checksum = "982977ebb49137e23fa29c8754c66d191b74c1221ae71bc71059704fc6d62ff5" dependencies = [ "encoding_rs", "indexmap", @@ -1570,9 +1585,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1581,9 +1596,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -1593,18 +1608,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -1614,9 +1629,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -1625,9 +1640,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -1636,9 +1651,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 54397c06..b2433151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ resolver = "3" metadata.crane.name = "cmtool" [workspace.package] -version = "0.1.4" +version = "0.1.5" edition = "2024" authors = ["Benjamin Casale"] description = "Compartment Modelling Approach tool" diff --git a/cmtool-assemble/src/data.rs b/cmtool-assemble/src/data.rs index 0ed27333..cc2cdb18 100644 --- a/cmtool-assemble/src/data.rs +++ b/cmtool-assemble/src/data.rs @@ -29,14 +29,14 @@ pub struct ParsedFeeds { #[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, + pub(crate) compartment_cumsum: HashMap, + pub(crate) total_number_compartment: usize, ///Reactor id of pfr names, needed to ensure global mass balance - pub pfr_names: Vec, + pub(crate) 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, + pub(crate) cm_case_only: Option, + pub(crate) is_two_phase_flow: bool, } impl DomainInfo { pub fn get_relative_compartment_number( @@ -48,6 +48,10 @@ impl DomainInfo { .get(reactor_id) .map(|cum_sum| relative_index + *cum_sum) } + + pub fn get_total_number_compartment(&self) -> usize { + self.total_number_compartment + } } ///Details about generated domain @@ -55,10 +59,27 @@ impl DomainInfo { #[derive(Clone, Serialize, Deserialize, Default)] pub struct DomainData { ///connections between partial flowmaps - pub connections: Option<[cmtool_data::RawDataFlux; 2]>, - pub info: DomainInfo, + pub(crate) info: DomainInfo, ///Information about feed of the resulting merged domain - pub feeds: Option, - pub case_path: String, + pub(crate) feeds: Option, + pub(crate) case_path: String, pub run_id: String, } + +impl DomainData { + pub fn feeds(&self) -> &Option { + &self.feeds + } + + pub fn info(&self) -> &DomainInfo { + &self.info + } + + pub fn get_case_path(&self) -> impl AsRef { + &self.case_path + } + + pub fn is_already_generated(&self) -> bool { + self.info().cm_case_only.is_some() + } +} diff --git a/cmtool-assemble/src/errors.rs b/cmtool-assemble/src/errors.rs new file mode 100644 index 00000000..c1ecb14d --- /dev/null +++ b/cmtool-assemble/src/errors.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::io; +use thiserror::Error; + +#[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), +} diff --git a/cmtool-assemble/src/generators.rs b/cmtool-assemble/src/generators.rs deleted file mode 100644 index 01cd9bef..00000000 --- a/cmtool-assemble/src/generators.rs +++ /dev/null @@ -1,537 +0,0 @@ -// 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) = - (CMAExportType::LiquidFlow, CMAExportType::LiquidVolume); - -const GAS_PAIR: (CMAExportType, CMAExportType) = (CMAExportType::GasFlow, CMAExportType::GasVolume); - -type PairType = (CMAExportType, CMAExportType); - -const PAIRS: (PairType, PairType) = (LIQUID_PAIR, GAS_PAIR); - -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())) -} - -///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(), - } - } - - fn write_phase( - dest: &str, - case: &mut CMCase, - phase: RawPhase, - relative_path: Option, - ) -> Result<(), CMError> { - let (flowp, volumep) = phase.write(dest)?; - let flowp = match &relative_path { - Some(rel) => format!("./{}/{}", rel, flowp), - None => format!("./{}", flowp), - }; - - let volumep = match relative_path { - Some(rel) => format!("./{}/{}", rel, volumep), - None => format!("./{}", volumep), - }; - case.add(CMExportType::Flow(phase.identifier).into(), &flowp); - case.add(CMExportType::Volume(phase.identifier).into(), &volumep); - - Ok(()) - } - - fn generate_0d_phase( - &mut self, - case: &mut CMCase, - volume: f64, - phase: PhaseCM, - dest: Option, - ) -> 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()); - - match dest { - Some(s) => Self::write_phase(&s, case, phase, None), - None => { - self.raw_phase.push(phase); - Ok(()) - } - } - } - - fn generate_0d( - &mut self, - liquid_volume: f64, - gas_volume: f64, - fields: Option<&[Field0D]>, - dest: Option, - ) -> Result { - let mut case = CMCase::default(); - case.n_div = [1, 0, 0]; - - self.generate_0d_phase(&mut case, liquid_volume, PhaseCM::Liquid, dest.clone())?; - - if gas_volume != 0. { - self.generate_0d_phase(&mut case, gas_volume, PhaseCM::Gas, dest)?; - } - - if let Some(_scalars) = fields { - todo!("Scalar field") - } - - Ok(case) - } - - pub fn generate_0d_from_fraction( - &mut self, - total_volume: f64, - gas_fraction: f64, - dest: Option, - ) -> Result { - if !(0. ..=1.).contains(&gas_fraction) { - panic!("TODO: handle error gas fraction generation 0d"); - } - - let gas_volume = gas_fraction * total_volume; - let liquid_volume = total_volume * (1. - gas_fraction); - - 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, - flow: f64, - volume_fraction: f64, - axial_dispersion: f64, - gas: bool, - dest: Option, - ) -> 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 = flow / reactor_section_area; - let n_flow = n_compartment - 1; - - let flow_source_target = reactor_section_area / dx * (flow_velocity + axial_dispersion); - let flow_target_source = reactor_section_area / dx * axial_dispersion; - - assert!( - ((compartment_volume * n_compartment as f64) - - (volume_fraction * reactor_section_area * length)) - .abs() - < 1e-5 - ); - - let cp = if gas { PhaseCM::Gas } else { PhaseCM::Liquid }; - let mut phase = RawPhase::new(n_compartment, n_flow, cp); - - //n_flow != n_compartment, need to set volume separately - phase.volume = RawDataScalar::from(vec![compartment_volume; n_compartment]); //Scalar values are not preallo - - for (current_index, flow) in phase.flow.fluxes.iter_mut().enumerate() { - flow.id_source = current_index as u32; - flow.id_target = (current_index + 1) as u32; - flow.flux_source_target = flow_source_target; - flow.flux_target_source = flow_target_source; - } - - if let Some(s) = dest { - Self::write_phase(&s, case, phase, None)?; - } else { - self.raw_phase.push(phase); - } - - Ok(()) - } - - pub fn generate_1d_from_fraction( - &mut self, - - PFRDescription { - n_compartment, - length, - diameter, - liquid_flow, - gas_flow, - gas_fraction, - axial_dispersion, - }: PFRDescription, - dest: Option, - ) -> Result { - let mut case = CMCase::default(); - case.n_div = [0, 0, n_compartment as u32]; - - if !(0. ..=1.).contains(&gas_fraction) { - panic!("TODO: handle error gas fraction generation 0d"); - } - - self.generate_1d( - &mut case, - n_compartment, - length, - diameter, - liquid_flow, - 1.0 - gas_fraction, - axial_dispersion, - false, - dest.clone(), - )?; - if gas_fraction != 0. { - self.generate_1d( - &mut case, - n_compartment, - length, - diameter, - gas_flow, - 1.0 - gas_fraction, - axial_dispersion, - true, - dest, - )?; - } - - Ok(case) - } - - fn merge_phase( - // flows: Vec, - // volumes: Vec - phases: Vec, - connections: Option, - ) -> Result { - let mut phase = RawPhase::new(0, 0, phases[0].identifier); - - // let mut merge_phase_flow = RawDataFlux::new(0, 0); // - // let mut merge_phase_volume = RawDataScalar::new(0); - let offset_compartment = std::cell::Cell::new(0u32); - let incr_id = |mut flow: RawFlux| -> RawFlux { - flow.id_source += offset_compartment.get(); - flow.id_target += offset_compartment.get(); - flow - }; - - for p in &phases { - let rd = &p.flow; - let v = &p.volume; - phase.flow.header.n_zone += rd.header.n_zone; - phase.volume.header.n_zone += rd.header.n_zone; - phase.flow.header.n_fluxes += rd.header.n_fluxes; - phase - .flow - .fluxes - .extend(rd.fluxes.iter().map(|&flux| incr_id(flux))); - phase.volume.values.extend(v.values.clone().into_iter()); - offset_compartment.set(offset_compartment.get() + rd.header.n_zone); - } - if let Some(connections) = connections { - phase.flow.header.n_fluxes += connections.header.n_fluxes; - phase.flow.fluxes.extend(connections.fluxes); - } - - if phase.flow.fluxes.len() != phase.flow.header.n_fluxes as usize { - panic!( - "TODO: handle merge error {} {}", - phase.flow.fluxes.len(), - phase.flow.header.n_fluxes - ); - } - - Ok(phase) - } - - pub fn merge_from_memory( - &self, - dest: &str, - connections: Option<[RawDataFlux; 2]>, - ) -> 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!("{}/{}", 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(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()); - - let phase = Self::merge_phase(liquid_phase, liquid_connection)?; - Self::write_phase(&path, &mut case, phase, relative.clone())?; - - if !gasphase.is_empty() { - let phase = Self::merge_phase(gasphase, gas_connection)?; - Self::write_phase(&path, &mut case, phase, relative)?; - } - 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( - &self, - dest: &str, - ids: &[String], - connections: Option<[RawDataFlux; 2]>, - ) -> 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()); - let mut gas_volumes = Vec::with_capacity(ids.len()); - - let mut n_div = [0, 0, 0]; - for id in ids.iter() { - 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 = 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] += 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 sc = RawDataScalar::read_raw(path) - .ok_or(CMError::Custom("Error reading".to_string()))?; - liquid_volumes.push(sc); - let (gas_flow, gas_volume) = PAIRS.1; - let path = resolve_path(&partial_case, &relative_path, gas_flow)?; - - let rf = match RawDataFlux::read_raw(path) { - Some(rf) => rf, - None => { - RawDataFlux::new(n_zone, 1) //Default implementation of RawFlux implies - //correct workaround - } - }; - gas_flows.push(rf); - - let path = resolve_path(&partial_case, &relative_path, gas_volume)?; - let rs = match RawDataScalar::read_raw(path) { - Some(rs) => rs, - None => { - let mut sc = RawDataScalar::new(n_zone); - sc.values.push((1e-9).into()); - sc - } - }; - gas_volumes.push(rs); - } - - let path = format!("{}/merged", dest); - std::fs::create_dir_all(&path)?; - let mut case = CMCase::default(); - case.n_div = n_div; - - 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()); - //TODO wont working without let relative = Some(String::from("merged")); - let phase = Self::merge_phase(liquid_phases, liquid_connection)?; - Self::write_phase(&path, &mut case, phase, None)?; - - if !gas_phases.is_empty() { - let phase = Self::merge_phase(gas_phases, gas_connection)?; - Self::write_phase(&path, &mut case, phase, None)?; - } - - 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(path) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - #[test] - fn test_0d() { - let path = "/tmp/test_0d"; - std::fs::create_dir_all(path).unwrap(); - - let case = Generator::new() - .generate_0d_from_fraction(10., 0.2, Some(path.to_owned())) - .expect("case"); - - let liquid_volume_path = case - .resolve(path, cmtool_data::CMAExportType::LiquidVolume) - .expect("path"); - - let liquid_volume = - cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()).expect("Liquid error"); - let gas_volume_path = case - .resolve(path, cmtool_data::CMAExportType::GasVolume) - .expect("path"); - - let gas_volume = - cmtool_data::RawDataScalar::read_raw(gas_volume_path.clone()).expect("gas_volume"); - std::fs::remove_dir_all(path).unwrap(); - - assert_eq!(gas_volume.values.len(), 1); - assert_eq!(gas_volume.values[0].value, 2.0); - assert_eq!(liquid_volume.values[0].value, 8.0); - } - #[test] - fn test_1d() { - let path = "/tmp/test_1d"; - std::fs::create_dir_all(path).unwrap(); - 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(desc, Some(path.to_owned())) - .expect("case"); - - 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") - .values - .iter() - .map(|v| v.value) - .sum(); - std::fs::remove_dir_all(path).unwrap(); - //volume is h*pi*d^2/4 - let geo_volume = l * (d * d) * std::f64::consts::PI / 4.; - - assert!(liquid_volume - (1. - alpha_g) * geo_volume < 1e-9); - } - - #[test] - fn test_merge_phase() { - let path = "/tmp/test_merge"; - std::fs::create_dir_all(path).unwrap(); - 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(desc, Some(path.to_owned())) - .expect("case"); - let liquid_volume_path = case - .resolve(path, cmtool_data::CMAExportType::LiquidVolume) - .expect("path"); - - let liquid_volume: f64 = cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()) - .expect("Liquid error") - .values - .iter() - .map(|v| v.value) - .sum(); - - //volume is h*pi*d^2/4 - let geo_volume = l * (d * d) * std::f64::consts::PI / 4.; - std::fs::remove_dir_all(path).unwrap(); - assert!( - liquid_volume - (1. - alpha_g) * geo_volume < 1e-9, - "liquid_volume {}, alpha {}, geo_volume {}", - liquid_volume, - alpha_g, - geo_volume - ); - } -} diff --git a/cmtool-assemble/src/generators/artefact.rs b/cmtool-assemble/src/generators/artefact.rs new file mode 100644 index 00000000..b9ebc6f0 --- /dev/null +++ b/cmtool-assemble/src/generators/artefact.rs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::CMError; +use cmtool_data::{ + CMCase, CMCaseJson, CMCaseWriter, CMExportType, DEFAULT_CASE_FILE_NAME, PhaseCM, RawPhase, +}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct GenerateContract { + case: CMCase, + liquid_phase: Option, + gas_phase: Option, + relative_path: Option, +} + +impl fmt::Debug for GenerateContract { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GenerateContract").finish() + } +} + +fn format_path_case(relative_path: &Option, file_name: &str) -> PathBuf { + relative_path + .as_ref() + .map(|rel| std::path::Path::new(&rel).join(file_name)) + .unwrap_or_else(|| std::path::Path::new(file_name).to_path_buf()) +} + +impl GenerateContract { + pub fn get_case(&self) -> &CMCase { + &self.case + } + + pub fn new_single_phase(case: CMCase, phase: RawPhase, relative_path: Option) -> Self { + if phase.identifier == PhaseCM::Liquid { + Self { + case, + liquid_phase: Some(phase), + gas_phase: None, + relative_path, + } + } else { + Self { + case, + liquid_phase: None, + gas_phase: Some(phase), + relative_path, + } + } + } + + pub fn new( + case: CMCase, + liquid_phase: RawPhase, + gas_phase: Option, + relative_path: Option, + ) -> Self { + Self { + case, + liquid_phase: Some(liquid_phase), + gas_phase, + relative_path, + } + } + + fn write_phase( + dest: impl AsRef, + case: &mut CMCase, + phase: RawPhase, + relative_path: Option, + ) -> Result<(), CMError> { + let (flowp, volumep) = phase.write(dest.as_ref().to_str().expect("utf path"))?; + + let flowp = format_path_case(&relative_path, &flowp); + let volumep = format_path_case(&relative_path, &volumep); + + case.add( + CMExportType::Flow(phase.identifier).into(), + flowp.to_str().expect("UTF-8 path"), + ); + case.add( + CMExportType::Volume(phase.identifier).into(), + volumep.to_str().expect("UTF-8 path"), + ); + + Ok(()) + } + + fn prepare_fs(&self, root_dir: impl AsRef) -> Result { + let path = if let Some(p) = &self.relative_path { + root_dir.as_ref().join(p) + } else { + root_dir.as_ref().to_owned() + }; + std::fs::create_dir_all(&path)?; //FIXME + Ok(path) + } + + pub fn write(mut self, root_dir: impl AsRef) -> Result { + if self.liquid_phase.is_none() && self.gas_phase.is_none() { + return Err(CMError::Custom("No phase to write".to_owned())); + } + + let path = self.prepare_fs(&root_dir)?; + + if let Some(liquid_phase) = self.liquid_phase { + Self::write_phase( + &path, + &mut self.case, + liquid_phase, + self.relative_path.clone(), + )?; + } + + if let Some(gas_phase) = self.gas_phase { + Self::write_phase(path, &mut self.case, gas_phase, self.relative_path.clone())?; + } + + let case_path = root_dir.as_ref().join(DEFAULT_CASE_FILE_NAME); + CMCaseJson::write_case(self.case.clone(), &case_path)?; + + Ok(self.case) + } +} diff --git a/cmtool-assemble/src/generators/descriptors.rs b/cmtool-assemble/src/generators/descriptors.rs new file mode 100644 index 00000000..c3840b56 --- /dev/null +++ b/cmtool-assemble/src/generators/descriptors.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use cmtool_data::PhaseCM; + +pub struct Reactor0DDescriptor { + liquid_volume: f64, + gas_volume: f64, +} + +impl Reactor0DDescriptor { + pub fn liquid_volume(&self) -> f64 { + self.liquid_volume + } + + pub fn gas_volume(&self) -> f64 { + self.gas_volume + } + + pub fn is_valid(&self) -> Result<(), String> { + if !self.liquid_volume.is_finite() || self.liquid_volume <= 0.0 { + return Err("Liquid volume must be a finite positive number".into()); + } + //TODO + if self.gas_volume < 0. { + return Err("Gas volume must be a finite positive number".into()); + } + + Ok(()) + } + + pub fn new(liquid_volume: L, gas_volume: G) -> Self + where + L: Into, + G: Into, + { + let _self = Self { + liquid_volume: liquid_volume.into(), + gas_volume: gas_volume.into(), + }; + _self.is_valid().unwrap(); //TODO + _self + } + + pub fn from_fraction(total_volume: f64, gas_fraction: f64) -> Self { + if !(0. ..=1.).contains(&gas_fraction) { + panic!("TODO: handle error gas fraction generation 0d"); + } + let gas_volume = gas_fraction * total_volume; + let liquid_volume = total_volume - gas_volume; + + Self::new(liquid_volume, gas_volume) + } +} + +pub struct PFRDescription { + pub(super) n_compartment: usize, + pub(super) length: f64, + pub(super) diameter: f64, + pub(super) liquid_flow: f64, + pub(super) gas_flow: f64, + pub(super) gas_fraction: f64, + pub(super) axial_dispersion: f64, +} + +impl PFRDescription { + pub fn new( + n_compartment: usize, + length: impl Into, + diameter: impl Into, + liquid_flow: impl Into, + gas_flow: impl Into, + gas_fraction: impl Into, + axial_dispersion: impl Into, + ) -> Result { + let pfr = Self { + n_compartment, + length: length.into(), + diameter: diameter.into(), + liquid_flow: liquid_flow.into(), + gas_flow: gas_flow.into(), + gas_fraction: gas_fraction.into(), + axial_dispersion: axial_dispersion.into(), + }; + + pfr.is_valid()?; + Ok(pfr) + } + + pub fn get_liquid_flow(&self) -> f64 { + self.liquid_flow + } + + pub fn get_gas_flow(&self) -> f64 { + self.gas_flow + } + pub fn get_gas_fraction(&self) -> f64 { + self.gas_fraction + } + pub fn get_liquid_fraction(&self) -> f64 { + 1. - self.gas_fraction + } + //volume is h*pi*d^2/4 + #[allow(unused)] + pub fn geometrical_volume(&self) -> f64 { + self.length * (self.diameter * self.diameter) * std::f64::consts::PI / 4. + } + + pub fn extract_volume_flow(&self, phase: PhaseCM) -> (f64, f64) { + match phase { + PhaseCM::Liquid => (self.get_liquid_fraction(), self.get_liquid_flow()), + PhaseCM::Gas => (self.get_gas_fraction(), self.get_gas_flow()), + } + } + + pub fn is_valid(&self) -> Result<(), String> { + if self.n_compartment == 0 { + return Err("n_compartment must be >= 1".into()); + } + if !self.length.is_finite() || self.length <= 0.0 { + return Err("length must be a finite positive number".into()); + } + if !self.diameter.is_finite() || self.diameter <= 0.0 { + return Err("diameter must be a finite positive number".into()); + } + if !self.liquid_flow.is_finite() || self.liquid_flow < 0.0 { + return Err("liquid_flow must be a finite non-negative number".into()); + } + if !self.gas_flow.is_finite() || self.gas_flow < 0.0 { + return Err("gas_flow must be a finite non-negative number".into()); + } + if !self.gas_fraction.is_finite() || self.gas_fraction < 0.0 || self.gas_fraction > 1.0 { + return Err("gas_fraction must be between 0.0 and 1.0".into()); + } + if !self.axial_dispersion.is_finite() || self.axial_dispersion < 0.0 { + return Err("axial_dispersion must be a finite non-negative number".into()); + } + + if self.liquid_flow == 0.0 && self.gas_flow == 0.0 { + return Err("At least one of liquid_flow or gas_flow must be positive".into()); + } + if (self.gas_flow > 0.0 && self.liquid_flow == 0.0) + && (self.gas_fraction < 0.0 || self.gas_fraction > 1.0) + { + return Err("Invalid gas_fraction given flows".into()); + } + + // geometric length should be >= diameter + if self.length < self.diameter { + return Err("length should be greater than or equal to diameter".into()); + } + + Ok(()) + } +} diff --git a/cmtool-assemble/src/generators/mod.rs b/cmtool-assemble/src/generators/mod.rs new file mode 100644 index 00000000..90177794 --- /dev/null +++ b/cmtool-assemble/src/generators/mod.rs @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::path::PathBuf; + +use crate::CMError; +mod artefact; +pub use artefact::GenerateContract; +mod descriptors; + +use cmtool_data::{ + CMAExportType, CMCase, CMCaseJson, CMCaseReader, DEFAULT_CASE_FILE_NAME, PhaseCM, RawData, + RawDataFlux, RawDataScalar, RawFlux, RawPhase, +}; +pub use descriptors::{PFRDescription, Reactor0DDescriptor}; + +const LIQUID_PAIR: (CMAExportType, CMAExportType) = + (CMAExportType::LiquidFlow, CMAExportType::LiquidVolume); + +const GAS_PAIR: (CMAExportType, CMAExportType) = (CMAExportType::GasFlow, CMAExportType::GasVolume); + +type PairType = (CMAExportType, CMAExportType); + +const PAIRS: (PairType, PairType) = (LIQUID_PAIR, GAS_PAIR); + +//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: impl AsRef, + value: CMAExportType, +) -> Result { + case.resolve(relative_path.as_ref().to_str().expect("UTF-8 path"), value) + .ok_or(CMError::Custom("Error resolving path".to_string())) +} + +///Create vector of raw phase from raw +fn raw_phase_from_flow_vol( + flows: Vec, + vol: Vec, + phase: PhaseCM, +) -> Vec { + flows + .into_iter() + .zip(vol) + .map(move |(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() +// } +fn filter_phase(raw_phase: &[RawPhase], phase: PhaseCM) -> impl Iterator { + raw_phase.iter().filter(move |p| p.identifier == phase) +} + +const MERGE_FOLDER_NAME: &str = "merged"; + +//public +impl Generator { + pub fn new() -> Self { + Self { + raw_phase: Default::default(), + } + } + + pub fn generate_0d( + &mut self, + descriptor: Reactor0DDescriptor, + dest: Option, + ) -> Result { + self.impl_generate_0d(descriptor, None, dest) + } + + pub fn generate_1d( + &mut self, + descriptor: PFRDescription, + out_dir: Option, + ) -> Result { + //Method expects descriptor has been validated with is_valid method + + let mut case = CMCase::default(); + case.n_div = [0, 0, descriptor.n_compartment as u32]; + + self.impl_generate_1d(&mut case, &descriptor, PhaseCM::Liquid, out_dir.clone())?; + if descriptor.gas_fraction != 0. { + self.impl_generate_1d(&mut case, &descriptor, PhaseCM::Gas, out_dir)?; + } + + Ok(case) + } + + pub fn merge_from_memory( + &self, + connections: Option<[RawDataFlux; 2]>, + ) -> Result { + let liquid_phase = filter_phase(&self.raw_phase, PhaseCM::Liquid); + let gas_phase = filter_phase(&self.raw_phase, PhaseCM::Gas); + let case = CMCase::default(); + let relative = Some(String::from(MERGE_FOLDER_NAME)); + + let (liquid_phase, gas_phase) = Self::impl_merge(liquid_phase, gas_phase, connections)?; + + Ok(GenerateContract::new( + case, + liquid_phase, + gas_phase, + relative, + )) + } + + pub fn merge( + &self, + root_dir: impl AsRef, + ids: &[String], + connections: Option<[RawDataFlux; 2]>, + ) -> Result { + //helpers + let read_flux = |partial_case: &CMCase, + relative_path: &std::path::PathBuf, + key: CMAExportType| + -> Result, CMError> { + let p = resolve_path(partial_case, relative_path, key)?; + Ok(RawDataFlux::read_raw(p)) + }; + + let read_scalar = |partial_case: &CMCase, + relative_path: &std::path::PathBuf, + key: CMAExportType| + -> Result, CMError> { + let p = resolve_path(partial_case, relative_path, key)?; + Ok(RawDataScalar::read_raw(p)) + }; + + let mut n_div = [0, 0, 0]; + //helper + let mut add_ndiv = |n: &[u32; 3]| { + n_div[0] += n[0]; + n_div[1] += n[1]; + n_div[2] += n[2]; + }; + + let read_partial_case = |relative_path: &PathBuf| { + let case_path = relative_path.join(DEFAULT_CASE_FILE_NAME); + CMCaseJson::read_case(&case_path) + }; + + //prealloc + 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()); + let mut gas_volumes = Vec::with_capacity(ids.len()); + + let mut impl_merge_reactor = |id: &str| -> Result<(), CMError> { + let relative_path = root_dir.as_ref().join(id); + let partial_case = read_partial_case(&relative_path)?; + + let (liquid_flow, liquid_volume) = PAIRS.0; + let (gas_flow, gas_volume) = PAIRS.1; + + let raw_liquid_flow = read_flux(&partial_case, &relative_path, liquid_flow)? + .ok_or_else(|| CMError::Custom("Error reading liquid flow".into()))?; + + let n_zone = raw_liquid_flow.header.n_zone as usize; + liquid_flows.push(raw_liquid_flow); + add_ndiv(&partial_case.n_div); + + let sc_liquid_volume = read_scalar(&partial_case, &relative_path, liquid_volume)? + .ok_or_else(|| CMError::Custom("Error reading liquid volume".into()))?; + liquid_volumes.push(sc_liquid_volume); + + // gas flow: fallback to default when missing + // Default implementation of RawFlux implies correct workaround + let raw_flow_gas = read_flux(&partial_case, &relative_path, gas_flow)? + .unwrap_or_else(|| RawDataFlux::new(n_zone, 1)); + gas_flows.push(raw_flow_gas); + + // gas volume: fallback to default scalar when missing + let sc_gas_volume = read_scalar(&partial_case, &relative_path, gas_volume)? + .unwrap_or_else(|| { + let mut sc = RawDataScalar::new(n_zone); + sc.values.push((1e-9).into()); + sc + }); + gas_volumes.push(sc_gas_volume); + + Ok(()) + }; + + ids.iter().try_for_each(|id| impl_merge_reactor(id))?; + + let merge_path = root_dir.as_ref().join(MERGE_FOLDER_NAME); + + std::fs::create_dir_all(&merge_path)?; + let mut case = CMCase::default(); + case.n_div = n_div; + + let liquid_phases = raw_phase_from_flow_vol(liquid_flows, liquid_volumes, PhaseCM::Liquid); + let gas_phases = raw_phase_from_flow_vol(gas_flows, gas_volumes, PhaseCM::Gas); + + let (liquid_phase, gas_phase) = + Self::impl_merge(liquid_phases.iter(), gas_phases.iter(), connections)?; + + Ok(GenerateContract::new( + // &path, + case, + liquid_phase, + gas_phase, + None, + )) + } +} + +//private impl +impl Generator { + fn generate_0d_phase( + &mut self, + case: &mut CMCase, + volume: f64, + phase: PhaseCM, + dest: Option, + ) -> 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()); + + match dest { + Some(s) => { + *case = GenerateContract::new_single_phase(case.clone(), phase, None).write(&s)?; + Ok(()) + } + None => { + self.raw_phase.push(phase); + Ok(()) + } + } + } + + fn impl_generate_0d( + &mut self, + descriptor: Reactor0DDescriptor, + fields: Option<&[Field0D]>, + dest: Option, + ) -> Result { + let mut case = CMCase::default(); + case.n_div = [1, 0, 0]; + + self.generate_0d_phase( + &mut case, + descriptor.liquid_volume(), + PhaseCM::Liquid, + dest.clone(), + )?; + let gas_volume = descriptor.gas_volume(); + if gas_volume != 0. { + self.generate_0d_phase(&mut case, gas_volume, PhaseCM::Gas, dest)?; + } + + if let Some(_scalars) = fields { + todo!("Scalar field") + } + + Ok(case) + } + + fn impl_generate_1d( + &mut self, + case: &mut CMCase, + descriptor: &PFRDescription, + phase_d: PhaseCM, + out_dir: Option, + ) -> Result<(), CMError> { + //Get needed info + let &PFRDescription { + n_compartment, + length, + diameter, + axial_dispersion, + .. + } = descriptor; + + let (volume_fraction, flow) = descriptor.extract_volume_flow(phase_d); + + debug_assert!(length > 0.); + debug_assert!(diameter > 0.); + debug_assert!(flow >= 0.); + debug_assert!(axial_dispersion >= 0.); + debug_assert!(volume_fraction <= 1. && volume_fraction > 0.); + + //Geometrical data + + 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 n_flow = n_compartment - 1; + + //PreCalculate velocity + let flow_velocity = flow / reactor_section_area; + let flow_source_target = reactor_section_area / dx * (flow_velocity + axial_dispersion); + let flow_target_source = reactor_section_area / dx * axial_dispersion; + + //Discretization should preserve geometrical volume + assert!( + ((compartment_volume * n_compartment as f64) + - (volume_fraction * reactor_section_area * length)) + .abs() + < 1e-5 + ); + + let mut phase = RawPhase::new(n_compartment, n_flow, phase_d); + + //n_flow != n_compartment, need to set volume separately + phase.volume = RawDataScalar::from(vec![compartment_volume; n_compartment]); //Scalar values are not preallo + + //Set flow for each compartment + for (current_index, flow) in phase.flow.fluxes.iter_mut().enumerate() { + flow.id_source = current_index as u32; + flow.id_target = (current_index + 1) as u32; + flow.flux_source_target = flow_source_target; + flow.flux_target_source = flow_target_source; + } + + //Select whether generate in place or defered + if let Some(s) = out_dir { + *case = GenerateContract::new_single_phase(case.clone(), phase, None).write(&s)?; + // GenerateContract::write_phase(&s, case, phase, None)?; + } else { + self.raw_phase.push(phase); + } + + Ok(()) + } + + fn merge_phase<'a, I>( + // flows: Vec, + // volumes: Vec + phases: I, + connections: Option, + ) -> Result + where + I: Iterator, + { + let phases = &mut phases.into_iter().peekable(); + + if phases.peek().is_none() { + return Err(CMError::Custom("Empty phases".to_owned())); + } + + let next = phases.peek().unwrap(); + + let mut phase = RawPhase::new(0, 0, next.identifier); + + let offset_compartment = std::cell::Cell::new(0u32); + let incr_id = |mut flow: RawFlux| -> RawFlux { + flow.id_source += offset_compartment.get(); + flow.id_target += offset_compartment.get(); + flow + }; + phases.for_each(|p| { + let rd = &p.flow; + let v = &p.volume; + phase.flow.header.n_zone += rd.header.n_zone; + phase.volume.header.n_zone += rd.header.n_zone; + phase.flow.header.n_fluxes += rd.header.n_fluxes; + phase + .flow + .fluxes + .extend(rd.fluxes.iter().map(|&flux| incr_id(flux))); + phase.volume.values.extend(v.values.clone()); + offset_compartment.set(offset_compartment.get() + rd.header.n_zone); + }); + + if let Some(connections) = connections { + phase.flow.header.n_fluxes += connections.header.n_fluxes; + phase.flow.fluxes.extend(connections.fluxes); + } + + if phase.flow.fluxes.len() != phase.flow.header.n_fluxes as usize { + panic!( + "TODO: handle merge error {} {}", + phase.flow.fluxes.len(), + phase.flow.header.n_fluxes + ); + } + + Ok(phase) + } + + fn impl_merge<'a>( + liquid_phase: impl Iterator, + gas_phase: impl Iterator, + connections: Option<[RawDataFlux; 2]>, + ) -> Result<(RawPhase, Option), CMError> { + let liquid_connection = connections.as_ref().map(|c| c[0].clone()); + let gas_connection = connections.as_ref().map(|c| c[1].clone()); + + let merged_liquid_phase = Self::merge_phase(liquid_phase, liquid_connection)?; + let gas_phase = &mut gas_phase.peekable(); + let merged_gas_phase = if gas_phase.peek().is_some() { + Some(Self::merge_phase(gas_phase, gas_connection)?) + } else { + None + }; + Ok((merged_liquid_phase, merged_gas_phase)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + fn g_descriptor_pfr() -> PFRDescription { + let l = 1.; + let d = 0.2; + let alpha_g = 0.1; + + PFRDescription::new(10, l, d, 0.01, 0.01, alpha_g, 1e-9).unwrap() + } + + #[test] + fn test_0d() { + let path = "/tmp/test_0d"; + std::fs::create_dir_all(path).unwrap(); + + let case = Generator::new() + .generate_0d( + Reactor0DDescriptor::from_fraction(10., 0.2), + Some(path.to_owned()), + ) + .expect("case"); + + let liquid_volume_path = case + .resolve(path, cmtool_data::CMAExportType::LiquidVolume) + .expect("path"); + + let liquid_volume = + cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()).expect("Liquid error"); + let gas_volume_path = case + .resolve(path, cmtool_data::CMAExportType::GasVolume) + .expect("path"); + + let gas_volume = + cmtool_data::RawDataScalar::read_raw(gas_volume_path.clone()).expect("gas_volume"); + std::fs::remove_dir_all(path).unwrap(); + + assert_eq!(gas_volume.values.len(), 1); + assert_eq!(gas_volume.values[0].value, 2.0); + assert_eq!(liquid_volume.values[0].value, 8.0); + } + + #[test] + fn test_merge_lazy() { + let path = "/tmp/test_merge_lazy"; + std::fs::create_dir_all(path).unwrap(); + //same value in g_descriptor_pfr + let l = 1.; + let d = 0.2; + + let desc = g_descriptor_pfr(); + let alpha_g = desc.get_gas_fraction(); + //volume is h*pi*d^2/4 + let geo_volume = l * (d * d) * std::f64::consts::PI / 4.; + assert!(geo_volume == desc.geometrical_volume()); + + let mut generator = Generator::new(); + let _case = generator.generate_1d(desc, None).expect("case"); + + let c = generator.merge_from_memory(None).expect("merge"); + + let case = c.write(path).expect("write"); + 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") + .values + .iter() + .map(|v| v.value) + .sum(); + std::fs::remove_dir_all(path).unwrap(); + + assert!(liquid_volume - (1. - alpha_g) * geo_volume < 1e-9); + } + + #[test] + fn test_merge_phase_2() { + let path = "/tmp/test_merge_phase_2"; + std::fs::create_dir_all(path).unwrap(); + + let v_0d = 10.; + let n_c = 10; + + let desc_pfr = g_descriptor_pfr(); + let alpha_g = desc_pfr.get_gas_fraction(); + let geo_volume = desc_pfr.geometrical_volume(); + let desc_0d = Reactor0DDescriptor::from_fraction(v_0d, alpha_g); + + let mut gene = Generator::new(); + + gene.generate_1d(desc_pfr, None).unwrap(); + gene.generate_0d(desc_0d, None).unwrap(); + + let gc = gene.merge_from_memory(None).unwrap(); + + let case = gc.write(path).unwrap(); + + let liquid_volume_path = case + .resolve(path, cmtool_data::CMAExportType::LiquidVolume) + .expect("path"); + + let liquid_volume = + cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()).expect("Liquid error"); + assert!(liquid_volume.header.n_zone as usize == n_c + 1); + + let total_volume: f64 = liquid_volume.values.iter().map(|v| v.value).sum(); + let expected_volume = (1. - alpha_g) * (geo_volume + v_0d); + assert!((total_volume - expected_volume).abs() < 1e-9); + + std::fs::remove_dir_all(path).unwrap(); + } + + #[test] + fn test_merge_phase_1() { + let path = "/tmp/test_merge"; + std::fs::create_dir_all(path).unwrap(); + let desc_pfr = g_descriptor_pfr(); + let alpha_g = desc_pfr.get_gas_fraction(); + let geo_volume = desc_pfr.geometrical_volume(); + + let case = Generator::new() + .generate_1d(desc_pfr, Some(path.to_owned())) + .expect("case"); + let liquid_volume_path = case + .resolve(path, cmtool_data::CMAExportType::LiquidVolume) + .expect("path"); + + let liquid_volume: f64 = cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()) + .expect("Liquid error") + .values + .iter() + .map(|v| v.value) + .sum(); + + //volume is h*pi*d^2/4 + std::fs::remove_dir_all(path).unwrap(); + assert!( + liquid_volume - (1. - alpha_g) * geo_volume < 1e-9, + "liquid_volume {}, alpha {}, geo_volume {}", + liquid_volume, + alpha_g, + geo_volume + ); + } + + #[test] + fn test_1d() { + let path = "/tmp/test_1d"; + std::fs::create_dir_all(path).unwrap(); + 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(desc, Some(path.to_owned())) + .expect("case"); + let liquid_volume_path = case + .resolve(path, cmtool_data::CMAExportType::LiquidVolume) + .expect("path"); + + let liquid_volume: f64 = cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()) + .expect("Liquid error") + .values + .iter() + .map(|v| v.value) + .sum(); + + let gas_volume_path = case + .resolve(path, cmtool_data::CMAExportType::GasVolume) + .expect("path"); + let gas_volume: f64 = cmtool_data::RawDataScalar::read_raw(gas_volume_path.clone()) + .expect("gas error") + .values + .iter() + .map(|v| v.value) + .sum(); + + //volume is h*pi*d^2/4 + let geo_volume = l * (d * d) * std::f64::consts::PI / 4.; + std::fs::remove_dir_all(path).unwrap(); + assert!( + (liquid_volume - (1. - alpha_g) * geo_volume).abs() < 1e-9, + "liquid_volume {}, alpha {}, geo_volume {}", + liquid_volume, + alpha_g, + geo_volume + ); + + assert!( + ((liquid_volume + gas_volume) - geo_volume).abs() < 1e-9, + "liquid_volume {}, gas_volume {}, geo_volume {}", + liquid_volume, + gas_volume, + geo_volume + ); + } +} diff --git a/cmtool-assemble/src/lib.rs b/cmtool-assemble/src/lib.rs index 731ab842..dea3561f 100644 --- a/cmtool-assemble/src/lib.rs +++ b/cmtool-assemble/src/lib.rs @@ -1,44 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later mod data; +mod errors; 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 cmtool_data::PhaseCM; //reexport to have easier dependency pub use data::{FeedFlow, ParsedFeeds}; +pub use errors::CMError; +pub use generators::GenerateContract; 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), -} +pub type ConnectionType = [cmtool_data::RawDataFlux; 2]; /// Domain parser // TODO: make it private + change name @@ -54,44 +29,58 @@ impl Parser { /// 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()) + fn continue_parsing( + p: Parser, + root_dir: impl AsRef, + ) -> Result<(DomainData, Option), CMError> { + let path = root_dir.as_ref().join(&p.0.run_id); + Self::continue_parsing_with_path(p, path) } /// 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) + root_dir: impl AsRef, + ) -> Result<(DomainData, Option), CMError> { + let (mut domain, mb, connections) = parse_domain(&root)?; + + let (path, gc): (std::path::PathBuf, Option) = + if let Some(cm_case) = &domain.info().cm_case_only { + (std::path::PathBuf::from(&cm_case), None) + } else { + //TODO: Do not create all, return error if not root_dir + // std::fs::create_dir_all(root_path)?; + let gc = generate_flowmap(None, &root.reactors, &mb, connections)?; + (root_dir.as_ref().to_owned(), gc) + }; + + domain.case_path = path.as_os_str().to_string_lossy().to_string(); + Ok((domain, gc)) } } //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)?; +pub fn generate_domain( + root_dir: impl AsRef, + reactor_content: &str, +) -> Result<(DomainData, Option), CMError> { + let (_id, parser) = Parser::start_parsing(reactor_content)?; - let domain = Parser::continue_parsing(root, root_dir)?; + Parser::continue_parsing(parser, root_dir) +} +//Parse and generate domain at root dir from give xml content +// Returns info about domain if suceeds +pub fn generate_and_write_domain( + root_dir: impl AsRef, + reactor_content: &str, +) -> Result { + let (domain, gc) = generate_domain(&root_dir, reactor_content)?; + if let Some(contract) = gc { + contract.write(root_dir)?; + } Ok(domain) } diff --git a/cmtool-assemble/src/map_generation.rs b/cmtool-assemble/src/map_generation.rs index 0a4fea47..5ff9fedf 100644 --- a/cmtool-assemble/src/map_generation.rs +++ b/cmtool-assemble/src/map_generation.rs @@ -1,12 +1,11 @@ // 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}; +use crate::{CMError, GenerateContract}; +use cmtool_data::DataError; +use cmtool_data::{CMCaseJson, DEFAULT_CASE_FILE_NAME, PhaseCM, RawDataFlux}; impl GeneralSizeType { ///Returns volume of reactor considering cylindrical shape @@ -24,51 +23,58 @@ impl GeneralSizeType { } fn _generate_reactor_0d( - save_intermediate: bool, generator: &mut Generator, - root: &str, + root: &Option>, 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)?; + let path = root.as_ref().map(|r| r.as_ref().join(reactor0d.id.clone())); - opt_path = Some(path.clone()); + let mut opt_path = None; + if let Some(p) = &path { + std::fs::create_dir_all(p).map_err(DataError::IO)?; + opt_path = Some(p.to_string_lossy().to_string()); } - let case = generator.generate_0d_from_fraction( + let descriptor = crate::generators::Reactor0DDescriptor::from_fraction( volume, reactor0d.volume_fraction.content as f64, - opt_path, - )?; + ); + + let case = generator.generate_0d(descriptor, opt_path)?; - if save_intermediate { - T::write_case(case, std::path::Path::new(&format!("{}/cma_case", path)))?; + // let case = generator.generate_0d_from_fraction( + // volume, + // reactor0d.volume_fraction.content as f64, + // opt_path, + // )?; + if let Some(p) = &path { + T::write_case(case, &p.join(DEFAULT_CASE_FILE_NAME))?; } Ok(()) } fn _generate_reactor_1d( - save_intermediate: bool, generator: &mut Generator, - root: &str, + root: &Option>, 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 path = root + .as_ref() + .map(|r| r.as_ref().join(current_pfr.id.clone())); let mut opt_path = None; - if save_intermediate { - std::fs::create_dir_all(path.clone()).map_err(DataError::IO)?; - opt_path = Some(path.clone()); + if let Some(p) = &path { + std::fs::create_dir_all(p).map_err(DataError::IO)?; + opt_path = Some(p.to_string_lossy().to_string()); } match ¤t_pfr.size { @@ -77,20 +83,21 @@ fn _generate_reactor_1d( } 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)))?; + let desc = PFRDescription::new( + current_pfr.compartments.get(), + dim.length.content, + dim.diameter.content, + mb.get_flow(¤t_pfr.id, PhaseCM::Liquid)?, + mb.get_flow(¤t_pfr.id, PhaseCM::Gas)?, + current_pfr.volume_fraction.content, + 1e-9, + ) + .map_err(|e| CMError::Custom(format!("Invalid PFR descritor {}", e)))?; + + let case: cmtool_data::CMCase = generator.generate_1d(desc, opt_path)?; + if let Some(p) = &path { + // T::write_case(case, std::path::Path::new(&format!("{}/cma_case", p)))?; + T::write_case(case, &p.join(DEFAULT_CASE_FILE_NAME))?; } } }; @@ -98,21 +105,19 @@ fn _generate_reactor_1d( } fn generate_partial_flowmap( - save_intermediate: bool, generator: &mut Generator, - root: &str, + root: Option>, 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)?; + _generate_reactor_0d::(generator, &root, &mut ids, r)?; } generated_domain::ReactorsTypeContent::Reactor1D(r) => { - _generate_reactor_1d::(save_intermediate, generator, root, &mut ids, r, mb)?; + _generate_reactor_1d::(generator, &root, &mut ids, r, mb)?; } generated_domain::ReactorsTypeContent::Reactor3D(reactor3_dtype) => { todo!("{:?}", reactor3_dtype) @@ -126,41 +131,48 @@ fn generate_partial_flowmap( } pub fn generate_flowmap( - root: &str, - domain: &DomainData, + root: Option, reactors: &generated_domain::ReactorsType, mb: &PfrGlobalMassBalance, -) -> Result { + connections: Option<[RawDataFlux; 2]>, +) -> Result, CMError> { let mut generator = Generator::new(); - let save_intermediate = reactors.content.len() == 1; - - let _ids = generate_partial_flowmap::( - save_intermediate, - &mut generator, - root, - reactors, - mb, - )?; + let save_intermediate = root.is_some(); + //root is used only if save_intermediae is true + let _ids = generate_partial_flowmap::(&mut generator, root.clone(), 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())) + // if _ids.len() > 1 { + // let gc = if save_intermediate { + // generator.merge(root.clone().unwrap(), &_ids, connections.clone())?; + // None + // } else { + // Some(generator.merge_from_memory(connections.clone())?) + // }; + // Ok((root.clone().unwrap().to_str().unwrap().to_owned(), gc)) + // } else if _ids.len() == 1 && save_intermediate { + // let r = root.unwrap(); + // let case_path = r.clone().join(&_ids[0]); + + // let prep = format!("./{}", _ids[0]); + // let case = CMCaseJson::read_case(&case_path)?.prepend_path(&prep); + // let path = r.join("cma_case"); + // CMCaseJson::write_case(case, &path)?; + // Ok((r.to_str().unwrap().to_owned(), None)) + // } else { + // Err(CMError::Custom("TODO ".to_owned())) + // } + if _ids.is_empty() { + return Err(CMError::Custom("No flowmap to generate".to_owned())); } + let gc = if save_intermediate { + generator.merge(root.clone().unwrap(), &_ids, connections.clone())?; + None + } else { + Some(generator.merge_from_memory(connections.clone())?) + }; + Ok(gc) } // fn parse_generate() diff --git a/cmtool-assemble/src/parser/mod.rs b/cmtool-assemble/src/parser/mod.rs index b3e5ce3b..01242f7e 100644 --- a/cmtool-assemble/src/parser/mod.rs +++ b/cmtool-assemble/src/parser/mod.rs @@ -6,11 +6,12 @@ use crate::{CMError, DomainData}; mod reactors; use reactors::{parse_connection, parse_feed, parse_reactor}; mod pfr_mb; +use crate::ConnectionType; pub(super) use pfr_mb::PfrGlobalMassBalance; pub fn parse_domain( root: &generated_domain::RootElementType, -) -> Result<(DomainData, PfrGlobalMassBalance), CMError> { +) -> Result<(DomainData, PfrGlobalMassBalance, Option), CMError> { if root.reactors.content.is_empty() { return Err(CMError::Parse(serde_xml_rs::Error::Custom( "At least one reactor required".to_owned(), @@ -34,13 +35,13 @@ pub fn parse_domain( let run_id = root.run_id.clone(); Ok(( DomainData { - connections: raw_connections, info, feeds: pfeeds, case_path: String::new(), run_id, }, mass_balance, + raw_connections, )) } diff --git a/cmtool-assemble/src/parser/reactors.rs b/cmtool-assemble/src/parser/reactors.rs index 1238cc7c..7380b113 100644 --- a/cmtool-assemble/src/parser/reactors.rs +++ b/cmtool-assemble/src/parser/reactors.rs @@ -13,6 +13,7 @@ use crate::{ CMError, data::{DomainInfo, FlowDirection}, }; +use cmtool_data::DEFAULT_CASE_FILE_NAME; use cmtool_data::{PhaseCM, RawDataFlux}; fn connection_per_phase( @@ -173,9 +174,7 @@ pub fn parse_reactor(reactors: &generated_domain::ReactorsType) -> Result { - let path = format!("{}/cma_case", reactor_from_file.path); - - let path = PathBuf::from(path); + let path = PathBuf::from(&reactor_from_file.path).join(DEFAULT_CASE_FILE_NAME); let case = cmtool_data::read_case(path.as_path())?; // case.n_compartment() domain_info diff --git a/cmtool-data/src/case.rs b/cmtool-data/src/case.rs index d1fe8e9f..4dd42457 100644 --- a/cmtool-data/src/case.rs +++ b/cmtool-data/src/case.rs @@ -55,6 +55,8 @@ impl std::fmt::Display for CMCase { } } +pub const DEFAULT_CASE_FILE_NAME: &str = "cma_case"; + impl CMCase { pub fn n_compartment(&self) -> u32 { if self.n_div.contains(&0) { @@ -342,7 +344,9 @@ mod test { fn commomn_read_test() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); // compile-time - let binding = Path::new(manifest_dir).join("test_data/cma_case"); + let binding = Path::new(manifest_dir) + .join("test_data") + .join(DEFAULT_CASE_FILE_NAME); let path = binding.as_path(); println!("{:?}", path); @@ -398,7 +402,9 @@ mod test { #[test] fn test_conversion() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); // compile-time - let binding = Path::new(manifest_dir).join("test_data/cma_case"); + let binding = Path::new(manifest_dir) + .join("test_data") + .join(DEFAULT_CASE_FILE_NAME); let c_path = binding.as_path(); let reference_case = CCMCaseInfo::read_case(c_path).unwrap(); diff --git a/cmtool-data/src/lib.rs b/cmtool-data/src/lib.rs index ac9766c4..dfa5a28d 100644 --- a/cmtool-data/src/lib.rs +++ b/cmtool-data/src/lib.rs @@ -6,7 +6,9 @@ mod flowmap; mod rawdata; mod states; mod transitioner; -pub use case::{CCMCaseInfo, CMCase, CMCaseJson, CMCaseReader, CMCaseWriter, read_case}; +pub use case::{ + CCMCaseInfo, CMCase, CMCaseJson, CMCaseReader, CMCaseWriter, DEFAULT_CASE_FILE_NAME, read_case, +}; use core::f64; pub use descriptors::{CMAExportType, CMExportType, PhaseCM}; pub use flowmap::FlowMapDescriptor; diff --git a/cmtool-data/src/states.rs b/cmtool-data/src/states.rs index 6ec60c2b..c795afae 100644 --- a/cmtool-data/src/states.rs +++ b/cmtool-data/src/states.rs @@ -195,9 +195,8 @@ impl IterationState { misc, }; - if ret.gas.is_some() { - let g = ret.gas.as_ref().unwrap(); - assert!(g.n_compartments() == ret.liquid.n_compartments()); + if let Some(gas1) = &ret.gas { + assert!(gas1.n_compartments() == ret.liquid.n_compartments()); } ret diff --git a/flake.nix b/flake.nix index 7d438fae..aa886ec4 100644 --- a/flake.nix +++ b/flake.nix @@ -107,12 +107,14 @@ devShells.default = craneLib.devShell { checks = self.checks.${system}; - packages = with pkgs; [ cargo-nextest - samply # profiling - + prek + samply ]; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.stdenv.cc.cc + ]; }; }); } diff --git a/meson.build b/meson.build index 1ac9df78..2d846a4f 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,8 @@ project( - 'rcmtool-cxx', - 'cpp', - version: '0.1.4', - meson_version: '>= 1.3.0', + 'rcmtool-cxx', + 'cpp', + version: '0.1.5', + meson_version: '>= 1.3.0', ) out_dir = meson.current_build_dir() + '/rust_target' root = meson.project_source_root() @@ -13,19 +13,19 @@ cpp = meson.get_compiler('cpp') _r = run_command('sh', '-c', f'mkdir -p @out_dir@/cxxbridge/cmtool-cxx/src', check: true).stdout() dummy = custom_target( - 'rust_build', - output: ['dummy'], - command: [ - cargo, - 'build', - '--manifest-path', cargo_path, - '--target-dir', out_dir, - '--profile', 'release-performance', - '-p', 'cmtool-cxx', - ], - build_always_stale: true, - build_by_default: true, - console: true, + 'rust_build', + output: ['dummy'], + command: [ + cargo, + 'build', + '--manifest-path', cargo_path, + '--target-dir', out_dir, + '--profile', 'release-performance', + '-p', 'cmtool-cxx', + ], + build_always_stale: true, + build_by_default: true, + console: true, ) dirs = include_directories('./rust_target/cxxbridge/cmtool-cxx/src') @@ -34,4 +34,4 @@ libdir = out_dir + '/release-performance/' rustlib = join_paths(libdir, 'libcmtool_cxx.a') -ext = declare_dependency(sources: [dummy], include_directories: dirs, link_args: [rustlib]) +ext = declare_dependency(sources: [dummy], include_directories: dirs, link_args: [rustlib]) \ No newline at end of file