feat: switch to Pert, add calendar scheduling
This commit is contained in:
18 files changed
+958
-323
No files matched your search
Generated
+366
-17
@@ -2,6 +2,21 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
@@ -51,6 +66,15 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
@@ -63,12 +87,33 @@ version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -76,10 +121,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.15"
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
@@ -108,6 +168,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
@@ -133,6 +199,17 @@ version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "distimate"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2e266cc5215f2a854deadfe97b5fb1047ab35121fd4fafa958e3760945790bf"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"statrs",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
@@ -178,6 +255,29 @@ version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.4.0"
|
||||
@@ -201,10 +301,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
name = "js-sys"
|
||||
version = "0.3.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.158"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -219,10 +328,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "mcps"
|
||||
version = "0.1.1"
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "matrixmultiply"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcps"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"distimate",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"rayon",
|
||||
@@ -230,6 +357,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"tempfile",
|
||||
"workdays",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -238,6 +366,63 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "nalgebra"
|
||||
version = "0.32.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"matrixmultiply",
|
||||
"nalgebra-macros",
|
||||
"num-complex",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"simba",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nalgebra-macros"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -254,6 +439,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
@@ -321,6 +512,12 @@ dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rawpointer"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
@@ -361,19 +558,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.207"
|
||||
name = "safe_arch"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||
checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.208"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.207"
|
||||
version = "1.0.208"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -382,9 +588,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.124"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
|
||||
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -405,6 +611,37 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simba"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"wide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "statrs"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f697a07e4606a0a25c044de247e583a330dbb1731d11bc7350b81f48ad567255"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"nalgebra",
|
||||
"num-traits",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -413,9 +650,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.74"
|
||||
version = "2.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -435,6 +672,32 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
@@ -459,6 +722,80 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||
|
||||
[[package]]
|
||||
name = "wide"
|
||||
version = "0.7.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"safe_arch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -541,6 +878,18 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "workdays"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e3bc4b29b64419a2983662ca6902adf5313df2a061e8f6104b2dd603765b5aa"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mcps"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
edition = "2021"
|
||||
description = "A command-line tool for running Monte Carlo simulations on project schedules."
|
||||
@@ -22,13 +22,16 @@ name = "mcps"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.38"
|
||||
clap = "4.5.15"
|
||||
distimate = "0.2.0"
|
||||
rand = "0.8.5"
|
||||
rand_distr = "0.4.3"
|
||||
rayon = "1.10.0"
|
||||
serde = { version = "1.0.207", features = ["derive"] }
|
||||
serde_json = "1.0.124"
|
||||
serde_yaml = "0.9.34"
|
||||
workdays = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.12.0"
|
||||
@@ -4,13 +4,13 @@ Monte Carlo Project Scheduler (mcps) is a command-line tool that runs Monte
|
||||
Carlo simulations on project schedules to estimate completion times and effort
|
||||
required. It helps project managers and engineers gain a better understanding of
|
||||
potential project risks and timelines by providing a probabilistic analysis of
|
||||
schedule durations and total work effort.
|
||||
project durations and total work effort.
|
||||
|
||||
## Features
|
||||
|
||||
- **Simulate Project Schedules**: Run multiple simulations to estimate the
|
||||
probability distribution of project completion times and total work effort.
|
||||
- **Customizable Inputs**: Accepts project schedules in YAML or JSON formats,
|
||||
- **Customizable Inputs**: Accepts project definitions in YAML or JSON formats,
|
||||
with options to override certain parameters like the number of workers or the
|
||||
number of iterations.
|
||||
- **Visual Output**: Generates an ASCII-based cumulative distribution function
|
||||
@@ -34,6 +34,9 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
cargo install mcps
|
||||
```
|
||||
|
||||
Note, if you're upgrading, use `cargo install --force mcps` to force
|
||||
installation of the latest version..
|
||||
|
||||
### Option 2: Clone and build the repository locally
|
||||
|
||||
```bash
|
||||
@@ -46,45 +49,79 @@ The binary will be available at `target/release/mcps`.
|
||||
|
||||
## Usage
|
||||
|
||||
Once you have built the tool, you can use it by providing a project schedule
|
||||
Once you have built the tool, you can use it by providing a project definition
|
||||
file in YAML or JSON format.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
mcps example.yaml -i 100000 -w 10
|
||||
mcps project.yaml -i 100000 -w 10
|
||||
```
|
||||
|
||||
This command runs the Monte Carlo simulation on the [`example.yaml`](./example.yaml) schedule file
|
||||
with 100,000 iterations and overrides the number of workers to 10.
|
||||
This command runs the Monte Carlo simulation on the
|
||||
[`project.yaml`](./assets/project.yaml) project file with 100,000 iterations
|
||||
and overrides the number of workers to 10.
|
||||
|
||||
### Command-Line Options
|
||||
|
||||
- `-i, --iterations <iterations>`: Specify the number of iterations to run. Must be at least 100. Default is 50,000.
|
||||
- `-w, --workers <num_workers>`: Override the number of workers specified in the schedule file.
|
||||
- `-v, --version`: Print version information.
|
||||
- `-h, --help`: Print help information.
|
||||
- `-i, --iterations <iterations>`: Specify the number of iterations to run. Must
|
||||
be at least 100. Default is 50,000.
|
||||
- `-n, --workers <num_workers>`: Override `num_workers` specified in project file
|
||||
- `-b, --begin <YYYY-MM-DD>`: Override `start_date` specified in project file
|
||||
- `-s, --schedule <filename>`: Work schedule config file (.yaml or .json)
|
||||
- `-h, --help`: Print help
|
||||
- `-V, --version`: Print version
|
||||
|
||||
### Schedule File Format
|
||||
### Project Definition File Format
|
||||
|
||||
The tool accepts schedule files in YAML or JSON format. Below is an example of the YAML format:
|
||||
`mcps` accepts projects in YAML or JSON format. Below is an example of the
|
||||
YAML format:
|
||||
|
||||
```yaml
|
||||
num_workers: 5 # for the purpose of scheduling simulation
|
||||
estimate_confidence: 0.80 # confidence actuals will fall inside estimate bounds
|
||||
start_date: 2024-08-01 # [optional] project start date
|
||||
tasks:
|
||||
- id: DesignPhase
|
||||
min_time: 1.5 # 1.5 days
|
||||
max_time: 3.5 # 3.5 days
|
||||
estimate:
|
||||
min: 1.5 # 1.5 days
|
||||
likely: 2.2 # 2.2 days (best guess at actual time)
|
||||
max: 3.5 # 3.5 days
|
||||
dependencies: []
|
||||
- id: ImplementationPhase
|
||||
min_time: 2.25 # 2.25 days
|
||||
max_time: 4.75 # 4.75 days
|
||||
estimate:
|
||||
min: 2.25 # 2.25 days
|
||||
likely: 3.9 # 3.9 days (best guess at actual time)
|
||||
max: 4.75 # 4.75 days
|
||||
dependencies: [DesignPhase]
|
||||
```
|
||||
|
||||
This repo includes examples in [JSON](./example.json) and
|
||||
[YAML](./example.yaml).
|
||||
This repo includes examples in [JSON](./assets/project.json) and
|
||||
[YAML](./assets/project.yaml).
|
||||
|
||||
### Work Schedule Configuration File Format
|
||||
|
||||
This file is optional. It allows you to configure the days of the week you work
|
||||
along with any specific work holidays.
|
||||
|
||||
If you don't provide a schedule file, the tool will default to a 5-day work week
|
||||
with no scheduled holidays.
|
||||
|
||||
`mcps` accepts schedules in YAML or JSON format. Below is an example of the
|
||||
YAML format:
|
||||
|
||||
work_days:
|
||||
|
||||
```yaml
|
||||
work_days:
|
||||
- Monday
|
||||
- Tuesday
|
||||
- Wednesday
|
||||
holidays:
|
||||
- 2023-12-25
|
||||
```
|
||||
|
||||
This repo includes examples in [JSON](./assets/schedule.json) and
|
||||
[YAML](./assets/schedule.yaml).
|
||||
|
||||
### Example Output
|
||||
|
||||
@@ -111,4 +148,5 @@ always welcome!
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
|
||||
This project is licensed under the MIT License. See the [LICENSE](./LICENSE)
|
||||
file for details.
|
||||
@@ -0,0 +1,40 @@
|
||||
# TODO
|
||||
|
||||
- update output with workdays results
|
||||
- add CSV output of distributions
|
||||
- add CLI parameter to specify which workdays, holidays, calendar config file to use
|
||||
- switch to BetaPERT distribution:
|
||||
|
||||
```rust
|
||||
use rand::Rng;
|
||||
|
||||
fn beta_pert_sample(min: f64, likely: f64, max: f64) -> f64 {
|
||||
let alpha1 = 1.0 + 4.0 * (likely - min) / (max - min);
|
||||
let alpha2 = 1.0 + 4.0 * (max - likely) / (max - min);
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let u: f64 = rng.gen(); // Generate a uniform random number between 0 and 1
|
||||
|
||||
// Inverse transform sampling
|
||||
let x = inverse_beta_cdf(u, alpha1, alpha2);
|
||||
|
||||
// Scale and shift to the desired range
|
||||
min + (max - min) * x
|
||||
}
|
||||
|
||||
fn inverse_beta_cdf(p: f64, alpha1: f64, alpha2: f64) -> f64 {
|
||||
// This is a simple approximation of the inverse beta CDF
|
||||
// For more accurate results, consider using a numerical method or a statistics library
|
||||
let x = p.powf(1.0 / alpha1) / (p.powf(1.0 / alpha1) + (1.0 - p).powf(1.0 / alpha2));
|
||||
x
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let min = 10.0;
|
||||
let likely = 20.0;
|
||||
let max = 40.0;
|
||||
|
||||
let sample = beta_pert_sample(min, likely, max);
|
||||
println!("Random sample from BetaPERT({}, {}, {}): {}", min, likely, max, sample);
|
||||
}
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 153 KiB |
@@ -1,49 +1,67 @@
|
||||
{
|
||||
"num_workers": 8,
|
||||
"estimate_confidence": 0.8,
|
||||
"start_date": "2024-08-01",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "RequirementsGathering",
|
||||
"min_time": 10.0,
|
||||
"max_time": 30.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"id": "SystemDesign",
|
||||
"min_time": 15.0,
|
||||
"max_time": 45.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"RequirementsGathering"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "DatabaseDesign",
|
||||
"min_time": 10.0,
|
||||
"max_time": 35.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"SystemDesign"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "UIUXDesign",
|
||||
"min_time": 20.0,
|
||||
"max_time": 50.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"RequirementsGathering"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "FrontendDevelopment",
|
||||
"min_time": 25.0,
|
||||
"max_time": 60.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"UIUXDesign"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BackendDevelopment",
|
||||
"min_time": 30.0,
|
||||
"max_time": 80.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"SystemDesign",
|
||||
"DatabaseDesign"
|
||||
@@ -51,16 +69,22 @@
|
||||
},
|
||||
{
|
||||
"id": "APIDevelopment",
|
||||
"min_time": 20.0,
|
||||
"max_time": 60.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"BackendDevelopment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "IntegrationTesting",
|
||||
"min_time": 15.0,
|
||||
"max_time": 45.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"FrontendDevelopment",
|
||||
"BackendDevelopment",
|
||||
@@ -69,32 +93,44 @@
|
||||
},
|
||||
{
|
||||
"id": "UserAcceptanceTesting",
|
||||
"min_time": 10.0,
|
||||
"max_time": 30.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"IntegrationTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PerformanceOptimization",
|
||||
"min_time": 10.0,
|
||||
"max_time": 40.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"IntegrationTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "SecurityAudit",
|
||||
"min_time": 10.0,
|
||||
"max_time": 30.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"IntegrationTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "FinalDeployment",
|
||||
"min_time": 5.0,
|
||||
"max_time": 15.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"UserAcceptanceTesting",
|
||||
"PerformanceOptimization",
|
||||
@@ -103,8 +139,11 @@
|
||||
},
|
||||
{
|
||||
"id": "ProjectRetrospective",
|
||||
"min_time": 3.0,
|
||||
"max_time": 10.0,
|
||||
"estimate": {
|
||||
"min": 10.0,
|
||||
"likely": 25.0,
|
||||
"max": 30.0
|
||||
},
|
||||
"dependencies": [
|
||||
"FinalDeployment"
|
||||
]
|
||||
@@ -1,129 +1,177 @@
|
||||
num_workers: 5
|
||||
estimate_confidence: 0.80
|
||||
start_date: 2024-08-01
|
||||
tasks:
|
||||
# Project Initiation and Planning
|
||||
- id: project_kickoff
|
||||
min_time: 2
|
||||
max_time: 3
|
||||
estimate:
|
||||
min: 2
|
||||
likely: 2.8
|
||||
max: 3
|
||||
dependencies: []
|
||||
|
||||
- id: requirements_gathering
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 13
|
||||
max: 15
|
||||
dependencies: [project_kickoff]
|
||||
|
||||
- id: system_architecture_design
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
estimate:
|
||||
min: 15
|
||||
likely: 18
|
||||
max: 20
|
||||
dependencies: [requirements_gathering]
|
||||
|
||||
- id: database_design
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 8
|
||||
max: 10
|
||||
dependencies: [system_architecture_design]
|
||||
|
||||
# Frontend Development
|
||||
- id: ui_ux_design
|
||||
min_time: 20
|
||||
max_time: 30
|
||||
estimate:
|
||||
min: 20
|
||||
likely: 24
|
||||
max: 30
|
||||
dependencies: [requirements_gathering]
|
||||
|
||||
- id: frontend_setup
|
||||
min_time: 3
|
||||
max_time: 5
|
||||
estimate:
|
||||
min: 3
|
||||
likely: 4.2
|
||||
max: 5
|
||||
dependencies: [ui_ux_design]
|
||||
|
||||
- id: user_authentication_frontend
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 6.1
|
||||
max: 10
|
||||
dependencies: [frontend_setup]
|
||||
|
||||
- id: product_catalog_frontend
|
||||
min_time: 15
|
||||
max_time: 25
|
||||
estimate:
|
||||
min: 15
|
||||
likely: 17
|
||||
max: 25
|
||||
dependencies: [frontend_setup]
|
||||
|
||||
- id: shopping_cart_frontend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [frontend_setup]
|
||||
|
||||
- id: checkout_process_frontend
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
estimate:
|
||||
min: 15
|
||||
likely: 17
|
||||
max: 20
|
||||
dependencies: [shopping_cart_frontend]
|
||||
|
||||
- id: user_profile_frontend
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 6
|
||||
max: 10
|
||||
dependencies: [user_authentication_frontend]
|
||||
|
||||
- id: search_functionality_frontend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [product_catalog_frontend]
|
||||
|
||||
# Backend Development
|
||||
- id: backend_setup
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 7
|
||||
max: 10
|
||||
dependencies: [system_architecture_design, database_design]
|
||||
|
||||
- id: user_authentication_backend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [backend_setup]
|
||||
|
||||
- id: product_catalog_backend
|
||||
min_time: 20
|
||||
max_time: 30
|
||||
estimate:
|
||||
min: 20
|
||||
likely: 25
|
||||
max: 30
|
||||
dependencies: [backend_setup]
|
||||
|
||||
- id: shopping_cart_backend
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
estimate:
|
||||
min: 15
|
||||
likely: 18
|
||||
max: 20
|
||||
dependencies: [backend_setup]
|
||||
|
||||
- id: checkout_process_backend
|
||||
min_time: 20
|
||||
max_time: 25
|
||||
estimate:
|
||||
min: 20
|
||||
likely: 23
|
||||
max: 25
|
||||
dependencies: [shopping_cart_backend]
|
||||
|
||||
- id: user_profile_backend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [user_authentication_backend]
|
||||
|
||||
- id: search_functionality_backend
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
estimate:
|
||||
min: 15
|
||||
likely: 18
|
||||
max: 20
|
||||
dependencies: [product_catalog_backend]
|
||||
|
||||
- id: payment_gateway_integration
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 13
|
||||
max: 15
|
||||
dependencies: [checkout_process_backend]
|
||||
|
||||
# API Development and Integration
|
||||
- id: api_design
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 7.5
|
||||
max: 10
|
||||
dependencies: [system_architecture_design]
|
||||
|
||||
- id: api_development
|
||||
min_time: 25
|
||||
max_time: 35
|
||||
estimate:
|
||||
min: 25
|
||||
likely: 32
|
||||
max: 35
|
||||
dependencies: [api_design, backend_setup]
|
||||
|
||||
- id: api_documentation
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 7
|
||||
max: 10
|
||||
dependencies: [api_development]
|
||||
|
||||
# Testing
|
||||
- id: unit_testing
|
||||
min_time: 20
|
||||
max_time: 30
|
||||
estimate:
|
||||
min: 20
|
||||
likely: 25
|
||||
max: 30
|
||||
dependencies:
|
||||
[
|
||||
user_authentication_backend,
|
||||
@@ -135,55 +183,75 @@ tasks:
|
||||
]
|
||||
|
||||
- id: integration_testing
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
estimate:
|
||||
min: 15
|
||||
likely: 16
|
||||
max: 20
|
||||
dependencies: [unit_testing, api_development]
|
||||
|
||||
- id: user_acceptance_testing
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [integration_testing]
|
||||
|
||||
# Security and Performance
|
||||
- id: security_audit
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [integration_testing]
|
||||
|
||||
- id: performance_optimization
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
estimate:
|
||||
min: 15
|
||||
likely: 17
|
||||
max: 20
|
||||
dependencies: [integration_testing]
|
||||
|
||||
# Deployment and Launch
|
||||
- id: deployment_planning
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 7
|
||||
max: 10
|
||||
dependencies: [system_architecture_design]
|
||||
|
||||
- id: staging_environment_setup
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 7
|
||||
max: 10
|
||||
dependencies: [deployment_planning]
|
||||
|
||||
- id: production_environment_setup
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [staging_environment_setup]
|
||||
|
||||
- id: data_migration
|
||||
min_time: 10
|
||||
max_time: 20
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 20
|
||||
dependencies: [production_environment_setup]
|
||||
|
||||
- id: user_training
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 7
|
||||
max: 10
|
||||
dependencies: [user_acceptance_testing]
|
||||
|
||||
- id: final_deployment
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
estimate:
|
||||
min: 5
|
||||
likely: 7
|
||||
max: 10
|
||||
dependencies:
|
||||
[
|
||||
user_acceptance_testing,
|
||||
@@ -195,11 +263,15 @@ tasks:
|
||||
|
||||
# Post-launch
|
||||
- id: post_launch_support
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
estimate:
|
||||
min: 10
|
||||
likely: 12
|
||||
max: 15
|
||||
dependencies: [final_deployment]
|
||||
|
||||
- id: project_retrospective
|
||||
min_time: 2
|
||||
max_time: 3
|
||||
estimate:
|
||||
min: 2
|
||||
likely: 2.8
|
||||
max: 3
|
||||
dependencies: [post_launch_support]
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"work_days": [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday"
|
||||
],
|
||||
"holidays": [
|
||||
"2023-12-25"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
work_days:
|
||||
- Monday
|
||||
- Tuesday
|
||||
- Wednesday
|
||||
holidays:
|
||||
- 2023-12-25
|
||||
+68
-63
@@ -1,52 +1,17 @@
|
||||
use mcps::{schedule::Schedule, simulation::run_multiple_simulations};
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use mcps::{schedule::Project, simulation::run_multiple_simulations};
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use workdays::WorkCalendar;
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
const AFTER_HELP_TEXT: &str = "\
|
||||
Examples of supported file formats:
|
||||
Find example project definition and schedule config files in the repository:
|
||||
|
||||
YAML Format:
|
||||
https://github.com/swaits/mcps/blob/main/examples/
|
||||
|
||||
num_workers: 5
|
||||
estimate_confidence: 0.80
|
||||
tasks:
|
||||
- id: DesignPhase
|
||||
min_time: 1.5 # 1.5 days
|
||||
max_time: 3.5 # 3.5 days
|
||||
dependencies: []
|
||||
- id: ImplementationPhase
|
||||
min_time: 2.25 # 2.25 days
|
||||
max_time: 4.75 # 4.75 days
|
||||
dependencies: [DesignPhase]
|
||||
|
||||
JSON Format:
|
||||
|
||||
{
|
||||
\"num_workers\": 5,
|
||||
\"estimate_confidence\": 0.80,
|
||||
\"tasks\": [
|
||||
{
|
||||
\"id\": \"DesignPhase\",
|
||||
\"min_time\": 1.5,
|
||||
\"max_time\": 3.5,
|
||||
\"dependencies\": []
|
||||
},
|
||||
{
|
||||
\"id\": \"ImplementationPhase\",
|
||||
\"min_time\": 2.25,
|
||||
\"max_time\": 4.75,
|
||||
\"dependencies\": [\"DesignPhase\"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Note: `estimate_confidence` is the confidence (0.0,1.0) that the
|
||||
actual values fall in the task estimate ranges. For example,
|
||||
0.8 means you believe that the actual time it will take to
|
||||
complete a task falls in your [min_time,max_time] estimate
|
||||
range for that task 80% of the time.";
|
||||
";
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let matches = Command::new("Monte Carlo Project Scheduler")
|
||||
@@ -55,7 +20,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.about("Runs Monte Carlo simulations on project schedules")
|
||||
.arg(
|
||||
Arg::new("filename")
|
||||
.help("Path to the schedule file (.yaml or .json).")
|
||||
.help("Path to the project file (.yaml or .json)")
|
||||
.required(true)
|
||||
.index(1),
|
||||
)
|
||||
@@ -68,15 +33,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)
|
||||
.arg(
|
||||
Arg::new("workers")
|
||||
.short('w')
|
||||
.short('n')
|
||||
.long("workers")
|
||||
.help("Override `num_workers` specified in schedule file")
|
||||
.help("Override `num_workers` specified in project file")
|
||||
.value_name("num_workers"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("begin")
|
||||
.short('b')
|
||||
.long("begin")
|
||||
.help("Override `start_date` specified in project file")
|
||||
.value_name("YYYY-MM-DD"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("schedule")
|
||||
.short('s')
|
||||
.long("schedule")
|
||||
.help("Work schedule config file (.yaml or .json)")
|
||||
.value_name("filename"),
|
||||
)
|
||||
.after_help(AFTER_HELP_TEXT)
|
||||
.get_matches();
|
||||
|
||||
let schedule_path = matches.get_one::<String>("filename").unwrap();
|
||||
let project_path = matches.get_one::<String>("filename").unwrap();
|
||||
let num_simulations: usize = matches.get_one::<String>("iterations").unwrap().parse()?;
|
||||
|
||||
// Ensure iterations is >= 100
|
||||
@@ -84,8 +63,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
return Err("Iterations must be at least 100.".into());
|
||||
}
|
||||
|
||||
// Load the schedule
|
||||
let mut schedule = Schedule::from_file(schedule_path)?;
|
||||
// Load the project
|
||||
let mut project = Project::from_file(project_path)?;
|
||||
|
||||
// Load work schedule if it exists
|
||||
let calendar: WorkCalendar = match matches.get_one::<String>("schedule") {
|
||||
Some(filename) => WorkCalendar::from_str(&std::fs::read_to_string(filename)?)?,
|
||||
None => WorkCalendar::new(),
|
||||
};
|
||||
|
||||
// Determine the start date (command line > project file > TODAY)
|
||||
let start_date = matches.get_one::<String>("begin")
|
||||
.and_then(|date| NaiveDate::parse_from_str(date, "%Y-%m-%d").ok())
|
||||
.or(project.start_date)
|
||||
.unwrap_or_else(|| Utc::now().date_naive());
|
||||
|
||||
// Check if workers are overridden by command-line argument
|
||||
if let Some(workers_str) = matches.get_one::<String>("workers") {
|
||||
@@ -93,34 +84,39 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if workers < 1 {
|
||||
return Err("Invalid number of workers, must be 1 or more".into());
|
||||
}
|
||||
schedule.num_workers = workers;
|
||||
project.num_workers = workers;
|
||||
}
|
||||
|
||||
// Monte Carlo simulation
|
||||
let (project_durations, effort_times) = run_multiple_simulations(&schedule, num_simulations);
|
||||
let (project_durations, effort_times) = run_multiple_simulations(&project, num_simulations);
|
||||
|
||||
// Results output
|
||||
print_ascii_cdf(
|
||||
&project_durations,
|
||||
format!(
|
||||
"Project Completion Time with {} Worker{} (by probability)",
|
||||
schedule.num_workers,
|
||||
if schedule.num_workers == 1 { "" } else { "s" }
|
||||
"Completion Time ({} Worker{}, starting {})",
|
||||
project.num_workers,
|
||||
if project.num_workers == 1 { "" } else { "s" },
|
||||
start_date,
|
||||
)
|
||||
.as_str(),
|
||||
"Duration",
|
||||
&start_date,
|
||||
&calendar,
|
||||
);
|
||||
|
||||
println!();
|
||||
|
||||
print_ascii_cdf(
|
||||
&effort_times,
|
||||
"Total Work Effort (by probability)",
|
||||
"Effort",
|
||||
format!("Total Work Effort (1 worker, starting {})", start_date).as_str(),
|
||||
&start_date,
|
||||
&calendar,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
|
||||
fn print_ascii_cdf(data: &[Duration], title: &str, start: &NaiveDate, calendar: &WorkCalendar) {
|
||||
let mut sorted_data = data.to_vec();
|
||||
sorted_data.sort_unstable();
|
||||
let min = sorted_data[0];
|
||||
@@ -139,9 +135,9 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
|
||||
padding_right = width - padding - title.len()
|
||||
);
|
||||
|
||||
println!("────┬────────────────────────────────────────────────────────────┬──────────");
|
||||
println!("%ile│{}│{:>10}", centered_title, units);
|
||||
println!("────┼────────────────────────────────────────────────────────────┼──────────");
|
||||
println!("────┬────────────────────────────────────────────────────────────┬──────────┬──────────┬──────────");
|
||||
println!("%ile│{}│ Workdays │ Schedule │ Complete ", centered_title);
|
||||
println!("────┼────────────────────────────────────────────────────────────┼──────────┼──────────┼──────────");
|
||||
|
||||
let bar_width = 60;
|
||||
|
||||
@@ -179,6 +175,9 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
|
||||
let offset = (bar_width - (max_bar_position - min_bar_position)) / 2;
|
||||
|
||||
for (i, (bar_position, days)) in bar_positions.iter().enumerate() {
|
||||
let (end_date, calendar_duration) =
|
||||
calendar.compute_end_date(*start, *days as i64).unwrap();
|
||||
|
||||
let shifted_bar_position = bar_position + offset;
|
||||
|
||||
let (fg, bg) = if i % 2 == 0 {
|
||||
@@ -191,7 +190,7 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
|
||||
0..=50 => "\x1b[31m", // Red
|
||||
55..=75 => "\x1b[33m", // Orange
|
||||
80..=90 => "\x1b[32m", // Green
|
||||
95..=100 => "\x1b[31m", // Red
|
||||
95..=100 => "\x1b[33m", // Orange
|
||||
_ => "\x1b[0m", // Default (shouldn't happen)
|
||||
};
|
||||
let reset_code = "\x1b[0m";
|
||||
@@ -205,7 +204,7 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
|
||||
.collect();
|
||||
|
||||
println!(
|
||||
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}",
|
||||
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}│{}{:5.0} days{}│{}{}{}",
|
||||
color_code,
|
||||
format!("p{}", 100 - i * 5),
|
||||
reset_code,
|
||||
@@ -214,8 +213,14 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
|
||||
reset_code,
|
||||
color_code,
|
||||
days,
|
||||
reset_code,
|
||||
color_code,
|
||||
calendar_duration.num_days(),
|
||||
reset_code,
|
||||
color_code,
|
||||
end_date,
|
||||
reset_code
|
||||
);
|
||||
}
|
||||
println!("────┴────────────────────────────────────────────────────────────┴──────────");
|
||||
println!("────┴────────────────────────────────────────────────────────────┴──────────┴──────────┴──────────");
|
||||
}
|
||||
+41
-38
@@ -1,25 +1,27 @@
|
||||
use crate::task::Task;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Schedule {
|
||||
pub struct Project {
|
||||
pub tasks: Vec<Task>,
|
||||
pub num_workers: usize,
|
||||
pub estimate_confidence: f64,
|
||||
pub start_date: Option<NaiveDate>,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
impl Project {
|
||||
pub fn new(
|
||||
tasks: Vec<Task>,
|
||||
num_workers: usize,
|
||||
estimate_confidence: f64,
|
||||
start_date: Option<NaiveDate>,
|
||||
) -> Result<Self, String> {
|
||||
let schedule = Schedule {
|
||||
let schedule = Project {
|
||||
tasks,
|
||||
num_workers,
|
||||
estimate_confidence,
|
||||
start_date,
|
||||
};
|
||||
schedule.validate()?;
|
||||
Ok(schedule)
|
||||
@@ -94,11 +96,12 @@ impl Schedule {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_task(id: &str, min: u64, max: u64, deps: Vec<&str>) -> Task {
|
||||
fn create_task(id: &str, min: u64, likely: u64, max: u64, deps: Vec<&str>) -> Task {
|
||||
Task {
|
||||
id: id.to_string(),
|
||||
dependencies: deps.into_iter().map(String::from).collect(),
|
||||
min_time: Duration::from_secs(min),
|
||||
likely_time: Duration::from_secs(likely),
|
||||
max_time: Duration::from_secs(max),
|
||||
}
|
||||
}
|
||||
@@ -106,25 +109,25 @@ mod tests {
|
||||
#[test]
|
||||
fn test_valid_schedule() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec![]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
create_task("C", 3, 5, vec!["A"]),
|
||||
create_task("A", 1, 2, 3, vec![]),
|
||||
create_task("B", 2, 3, 4, vec!["A"]),
|
||||
create_task("C", 3, 4, 5, vec!["A"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
let schedule = Project::new(tasks, 2, None);
|
||||
assert!(schedule.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_task_list() {
|
||||
let schedule = Schedule::new(vec![], 2, 0.8);
|
||||
let schedule = Project::new(vec![], 2, None);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(schedule.unwrap_err(), "Empty task list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_task_duration() {
|
||||
let tasks = vec![create_task("A", 3, 1, vec![])];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
let tasks = vec![create_task("A", 3, 2, 1, vec![])];
|
||||
let schedule = Project::new(tasks, 2, None);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(
|
||||
schedule.unwrap_err(),
|
||||
@@ -134,8 +137,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_zero_duration() {
|
||||
let tasks = vec![create_task("A", 0, 1, vec![])];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
let tasks = vec![create_task("A", 0, 1, 2, vec![])];
|
||||
let schedule = Project::new(tasks, 2, None);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(schedule.unwrap_err(), "Invalid task duration for task A");
|
||||
}
|
||||
@@ -143,10 +146,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_missing_dependency() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec![]),
|
||||
create_task("B", 2, 4, vec!["C"]),
|
||||
create_task("A", 1, 2, 3, vec![]),
|
||||
create_task("B", 2, 3, 4, vec!["C"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
let schedule = Project::new(tasks, 2, None);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(schedule.unwrap_err(), "Missing dependency C for task B");
|
||||
}
|
||||
@@ -154,10 +157,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_cyclic_dependency() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec!["B"]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
create_task("A", 1, 2, 3, vec!["B"]),
|
||||
create_task("B", 2, 3, 4, vec!["A"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
let schedule = Project::new(tasks, 2, None);
|
||||
assert!(schedule.is_err());
|
||||
assert!(schedule
|
||||
.unwrap_err()
|
||||
@@ -167,28 +170,28 @@ mod tests {
|
||||
#[test]
|
||||
fn test_complex_valid_schedule() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec![]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
create_task("C", 3, 5, vec!["A"]),
|
||||
create_task("D", 2, 6, vec!["B", "C"]),
|
||||
create_task("E", 1, 2, vec!["A"]),
|
||||
create_task("F", 2, 4, vec!["D", "E"]),
|
||||
create_task("A", 1, 2, 3, vec![]),
|
||||
create_task("B", 2, 3, 4, vec!["A"]),
|
||||
create_task("C", 3, 4, 5, vec!["A"]),
|
||||
create_task("D", 2, 4, 6, vec!["B", "C"]),
|
||||
create_task("E", 1, 2, 4, vec!["A"]),
|
||||
create_task("F", 2, 3, 4, vec!["D", "E"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 3, 0.9);
|
||||
let schedule = Project::new(tasks, 3, None);
|
||||
assert!(schedule.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_cyclic_dependency() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec!["F"]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
create_task("C", 3, 5, vec!["B"]),
|
||||
create_task("D", 2, 6, vec!["C"]),
|
||||
create_task("E", 1, 2, vec!["D"]),
|
||||
create_task("F", 2, 4, vec!["E"]),
|
||||
create_task("A", 1, 2, 3, vec!["F"]),
|
||||
create_task("B", 2, 3, 4, vec!["A"]),
|
||||
create_task("C", 3, 4, 5, vec!["B"]),
|
||||
create_task("D", 2, 3, 6, vec!["C"]),
|
||||
create_task("E", 1, 2, 3, vec!["D"]),
|
||||
create_task("F", 2, 3, 4, vec!["E"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 3, 0.9);
|
||||
let schedule = Project::new(tasks, 3, None);
|
||||
assert!(
|
||||
schedule.is_err(),
|
||||
"Expected cyclic dependency error, but got Ok"
|
||||
@@ -204,8 +207,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_self_dependency() {
|
||||
let tasks = vec![create_task("A", 1, 3, vec!["A"])];
|
||||
let schedule = Schedule::new(tasks, 1, 0.8);
|
||||
let tasks = vec![create_task("A", 1, 2, 3, vec!["A"])];
|
||||
let schedule = Project::new(tasks, 1, None);
|
||||
assert!(
|
||||
schedule.is_err(),
|
||||
"Expected self-dependency error, but got Ok"
|
||||
|
||||
+55
-35
@@ -1,5 +1,6 @@
|
||||
use crate::{schedule::Schedule, task::Task};
|
||||
use crate::{schedule::Project, task::Task};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::{fs::File, io::Read, path::Path, time::Duration};
|
||||
@@ -7,15 +8,23 @@ use std::{fs::File, io::Read, path::Path, time::Duration};
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct ScheduleInput {
|
||||
num_workers: usize,
|
||||
estimate_confidence: f64,
|
||||
start_date: Option<NaiveDate>,
|
||||
tasks: Vec<TaskInput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct EstimateInput {
|
||||
min: f64,
|
||||
max: f64,
|
||||
likely: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct TaskInput {
|
||||
id: String,
|
||||
min_time: f64,
|
||||
max_time: f64,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
estimate: EstimateInput,
|
||||
dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -24,7 +33,7 @@ enum FileFormat {
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
impl Project {
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut file = File::open(&path)?;
|
||||
let mut contents = String::new();
|
||||
@@ -41,13 +50,14 @@ impl Schedule {
|
||||
.into_iter()
|
||||
.map(|t| Task {
|
||||
id: t.id,
|
||||
min_time: Duration::from_secs_f64(t.min_time * 24.0 * 60.0 * 60.0),
|
||||
max_time: Duration::from_secs_f64(t.max_time * 24.0 * 60.0 * 60.0),
|
||||
min_time: Duration::from_secs_f64(t.estimate.min * 24.0 * 60.0 * 60.0),
|
||||
likely_time: Duration::from_secs_f64(t.estimate.likely * 24.0 * 60.0 * 60.0),
|
||||
max_time: Duration::from_secs_f64(t.estimate.max * 24.0 * 60.0 * 60.0),
|
||||
dependencies: t.dependencies,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let schedule = Schedule::new(tasks, input.num_workers, input.estimate_confidence)?;
|
||||
let schedule = Project::new(tasks, input.num_workers, input.start_date)?;
|
||||
|
||||
schedule.validate()?;
|
||||
|
||||
@@ -89,22 +99,24 @@ mod tests {
|
||||
fn test_load_from_yaml() {
|
||||
let yaml_content = r#"
|
||||
num_workers: 3
|
||||
estimate_confidence: 0.9
|
||||
tasks:
|
||||
- id: A
|
||||
min_time: 1
|
||||
max_time: 3
|
||||
estimate:
|
||||
min: 1
|
||||
likely: 2.4
|
||||
max: 3
|
||||
dependencies: []
|
||||
- id: B
|
||||
min_time: 2
|
||||
max_time: 4
|
||||
estimate:
|
||||
min: 2
|
||||
likely: 2.4
|
||||
max: 4
|
||||
dependencies: [A]
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(yaml_content, "yaml");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
let schedule = Project::from_file(path).unwrap();
|
||||
assert_eq!(schedule.tasks.len(), 2);
|
||||
assert_eq!(schedule.num_workers, 3);
|
||||
assert_eq!(schedule.estimate_confidence, 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -112,35 +124,39 @@ tasks:
|
||||
let json_content = r#"
|
||||
{
|
||||
"num_workers": 3,
|
||||
"estimate_confidence": 0.9,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "A",
|
||||
"min_time": 1,
|
||||
"max_time": 3,
|
||||
"estimate": {
|
||||
"min": 1,
|
||||
"likely": 1.8,
|
||||
"max": 3
|
||||
},
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"min_time": 2,
|
||||
"max_time": 4,
|
||||
"estimate": {
|
||||
"min": 2,
|
||||
"likely": 1.8,
|
||||
"max": 4
|
||||
},
|
||||
"dependencies": ["A"]
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(json_content, "json");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
let schedule = Project::from_file(path).unwrap();
|
||||
assert_eq!(schedule.tasks.len(), 2);
|
||||
assert_eq!(schedule.num_workers, 3);
|
||||
assert_eq!(schedule.estimate_confidence, 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_format() {
|
||||
let content = "irrelevant content";
|
||||
let (_temp_file, path) = create_temp_file(content, "txt");
|
||||
let result = Schedule::from_file(path);
|
||||
let result = Project::from_file(path);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
@@ -152,15 +168,16 @@ tasks:
|
||||
fn test_invalid_schedule() {
|
||||
let yaml_content = r#"
|
||||
num_workers: 3
|
||||
estimate_confidence: 0.9
|
||||
tasks:
|
||||
- id: A
|
||||
min_time: 3
|
||||
max_time: 1 # Invalid: min_time > max_time
|
||||
estimate:
|
||||
min: 3
|
||||
likely: 2
|
||||
max: 1 # Invalid: min_time > max_time
|
||||
dependencies: []
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(yaml_content, "yaml");
|
||||
let result = Schedule::from_file(path);
|
||||
let result = Project::from_file(path);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
@@ -172,15 +189,16 @@ tasks:
|
||||
fn test_load_with_floating_point_times_yaml() {
|
||||
let yaml_content = r#"
|
||||
num_workers: 3
|
||||
estimate_confidence: 0.9
|
||||
tasks:
|
||||
- id: A
|
||||
min_time: 1.5 # 1.5 days
|
||||
max_time: 3.5 # 3.5 days
|
||||
estimate:
|
||||
min: 1.5 # 1.5 days
|
||||
likely: 2 # 2.0 days
|
||||
max: 3.5 # 3.5 days
|
||||
dependencies: []
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(yaml_content, "yaml");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
let schedule = Project::from_file(path).unwrap();
|
||||
|
||||
// Verify that there is one task and it has the correct times
|
||||
assert_eq!(schedule.tasks.len(), 1);
|
||||
@@ -199,19 +217,21 @@ tasks:
|
||||
let json_content = r#"
|
||||
{
|
||||
"num_workers": 3,
|
||||
"estimate_confidence": 0.9,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "A",
|
||||
"min_time": 1.5,
|
||||
"max_time": 3.5,
|
||||
"estimate": {
|
||||
"min": 1.5,
|
||||
"likely": 3,
|
||||
"max": 3.5
|
||||
},
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(json_content, "json");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
let schedule = Project::from_file(path).unwrap();
|
||||
|
||||
// Verify that there is one task and it has the correct times
|
||||
assert_eq!(schedule.tasks.len(), 1);
|
||||
|
||||
+62
-34
@@ -1,7 +1,7 @@
|
||||
use crate::schedule::Schedule;
|
||||
use crate::schedule::Project;
|
||||
|
||||
use distimate::Pert;
|
||||
use rand::prelude::*;
|
||||
use rand_distr::{Distribution, Normal};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use std::{
|
||||
@@ -9,13 +9,14 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SimulationResult {
|
||||
pub total_project_duration: Duration,
|
||||
pub total_effort_time: Duration,
|
||||
}
|
||||
|
||||
pub fn run_multiple_simulations(
|
||||
schedule: &Schedule,
|
||||
schedule: &Project,
|
||||
num_simulations: usize,
|
||||
) -> (Vec<Duration>, Vec<Duration>) {
|
||||
(0..num_simulations)
|
||||
@@ -27,7 +28,7 @@ pub fn run_multiple_simulations(
|
||||
.unzip()
|
||||
}
|
||||
|
||||
fn run_simulation(schedule: &Schedule) -> SimulationResult {
|
||||
fn run_simulation(schedule: &Project) -> SimulationResult {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Simulate task times
|
||||
@@ -35,12 +36,7 @@ fn run_simulation(schedule: &Schedule) -> SimulationResult {
|
||||
.tasks
|
||||
.iter()
|
||||
.map(|task| {
|
||||
let task_time = simulate_task_time(
|
||||
&mut rng,
|
||||
task.min_time,
|
||||
task.max_time,
|
||||
schedule.estimate_confidence,
|
||||
);
|
||||
let task_time = simulate_task_time(&mut rng, task.min_time, task.max_time);
|
||||
(&task.id, task_time)
|
||||
})
|
||||
.collect();
|
||||
@@ -117,21 +113,13 @@ fn run_simulation(schedule: &Schedule) -> SimulationResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn simulate_task_time(
|
||||
rng: &mut impl Rng,
|
||||
min_time: Duration,
|
||||
max_time: Duration,
|
||||
estimate_confidence: f64,
|
||||
) -> Duration {
|
||||
fn simulate_task_time(rng: &mut impl Rng, min_time: Duration, max_time: Duration) -> Duration {
|
||||
let min_secs = min_time.as_secs_f64();
|
||||
let max_secs = max_time.as_secs_f64();
|
||||
let mean = (min_secs + max_secs) / 2.0;
|
||||
|
||||
let z_score = (1.0 - (1.0 - estimate_confidence) / 2.0).sqrt() * 2.0;
|
||||
let std_dev = (max_secs - min_secs) / (2.0 * z_score);
|
||||
|
||||
let normal = Normal::new(mean, std_dev).unwrap();
|
||||
let sampled_secs = normal.sample(rng).clamp(min_secs, max_secs);
|
||||
let pert = Pert::new(min_secs, mean, max_secs).unwrap();
|
||||
let sampled_secs = pert.sample(rng);
|
||||
|
||||
Duration::from_secs_f64(sampled_secs)
|
||||
}
|
||||
@@ -146,10 +134,9 @@ mod tests {
|
||||
let mut rng = thread_rng();
|
||||
let min_time = Duration::from_secs(5);
|
||||
let max_time = Duration::from_secs(15);
|
||||
let estimate_confidence = 0.8;
|
||||
|
||||
for _ in 0..1000 {
|
||||
let time = simulate_task_time(&mut rng, min_time, max_time, estimate_confidence);
|
||||
let time = simulate_task_time(&mut rng, min_time, max_time);
|
||||
assert!(
|
||||
time >= min_time && time <= max_time,
|
||||
"Simulated time should be within range"
|
||||
@@ -159,8 +146,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_single_task_simulation() {
|
||||
let task = Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(15));
|
||||
let schedule = Schedule::new(vec![task], 1, 0.8).unwrap();
|
||||
let task = Task::new(
|
||||
"A",
|
||||
vec![],
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(15),
|
||||
);
|
||||
let schedule = Project::new(vec![task], 1, None).unwrap();
|
||||
|
||||
let result = run_simulation(&schedule);
|
||||
|
||||
@@ -178,11 +171,29 @@ mod tests {
|
||||
#[test]
|
||||
fn test_multiple_independent_tasks() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(10)),
|
||||
Task::new("B", vec![], Duration::from_secs(7), Duration::from_secs(12)),
|
||||
Task::new("C", vec![], Duration::from_secs(3), Duration::from_secs(8)),
|
||||
Task::new(
|
||||
"A",
|
||||
vec![],
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(6),
|
||||
Duration::from_secs(10),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec![],
|
||||
Duration::from_secs(7),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(12),
|
||||
),
|
||||
Task::new(
|
||||
"C",
|
||||
vec![],
|
||||
Duration::from_secs(3),
|
||||
Duration::from_secs(6),
|
||||
Duration::from_secs(8),
|
||||
),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8).unwrap();
|
||||
let schedule = Project::new(tasks, 2, None).unwrap();
|
||||
|
||||
let result = run_simulation(&schedule);
|
||||
|
||||
@@ -199,32 +210,42 @@ mod tests {
|
||||
#[test]
|
||||
fn test_tasks_with_dependencies() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(10)),
|
||||
Task::new(
|
||||
"A",
|
||||
vec![],
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(8),
|
||||
Duration::from_secs(10),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
Duration::from_secs(7),
|
||||
Duration::from_secs(9),
|
||||
Duration::from_secs(12),
|
||||
),
|
||||
Task::new(
|
||||
"C",
|
||||
vec!["A".to_string()],
|
||||
Duration::from_secs(3),
|
||||
Duration::from_secs(6),
|
||||
Duration::from_secs(8),
|
||||
),
|
||||
Task::new(
|
||||
"D",
|
||||
vec!["B".to_string(), "C".to_string()],
|
||||
Duration::from_secs(4),
|
||||
Duration::from_secs(7),
|
||||
Duration::from_secs(9),
|
||||
),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8).unwrap();
|
||||
let schedule = Project::new(tasks, 2, None).unwrap();
|
||||
|
||||
let result = run_simulation(&schedule);
|
||||
println!("{:?}", result);
|
||||
|
||||
assert!(
|
||||
result.total_project_duration >= Duration::from_secs(14),
|
||||
result.total_project_duration >= Duration::from_secs(13),
|
||||
"Project duration should be at least the minimum critical path duration"
|
||||
);
|
||||
}
|
||||
@@ -232,15 +253,22 @@ mod tests {
|
||||
#[test]
|
||||
fn test_multiple_simulations_consistency() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(10)),
|
||||
Task::new(
|
||||
"A",
|
||||
vec![],
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(8),
|
||||
Duration::from_secs(10),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
Duration::from_secs(7),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(12),
|
||||
),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 1, 0.8).unwrap();
|
||||
let schedule = Project::new(tasks, 1, None).unwrap();
|
||||
|
||||
let (durations, efforts) = run_multiple_simulations(&schedule, 1000);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ pub struct Task {
|
||||
pub id: String,
|
||||
pub dependencies: Vec<String>,
|
||||
pub min_time: Duration,
|
||||
pub likely_time: Duration,
|
||||
pub max_time: Duration,
|
||||
}
|
||||
|
||||
@@ -13,12 +14,14 @@ impl Task {
|
||||
id: &str,
|
||||
dependencies: Vec<String>,
|
||||
min_time: Duration,
|
||||
likely_time: Duration,
|
||||
max_time: Duration,
|
||||
) -> Self {
|
||||
Task {
|
||||
id: id.to_string(),
|
||||
dependencies,
|
||||
min_time,
|
||||
likely_time,
|
||||
max_time,
|
||||
}
|
||||
}
|
||||
@@ -51,6 +54,7 @@ mod tests {
|
||||
"Task1",
|
||||
vec!["Dep1".to_string(), "Dep2".to_string()],
|
||||
Duration::from_secs(1),
|
||||
Duration::from_secs(3),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
@@ -66,6 +70,7 @@ mod tests {
|
||||
"Task2",
|
||||
vec![],
|
||||
Duration::from_secs(2),
|
||||
Duration::from_secs(7),
|
||||
Duration::from_secs(10),
|
||||
);
|
||||
|
||||
@@ -82,6 +87,7 @@ mod tests {
|
||||
vec!["Dep3".to_string()],
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
assert_eq!(task.id, "Task3");
|
||||
@@ -95,6 +101,7 @@ mod tests {
|
||||
"Task4",
|
||||
vec!["Dep4".to_string()],
|
||||
Duration::from_secs(3),
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(7),
|
||||
);
|
||||
let task2 = task1.clone();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
schedule::Project,
|
||||
simulation::run_multiple_simulations,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
@@ -12,7 +12,6 @@ fn test_average_effort_calculation() {
|
||||
let min_days = 5.0;
|
||||
let max_days = 15.0;
|
||||
let expected_avg_days = 10.0;
|
||||
let estimate_confidence = 0.8;
|
||||
let num_simulations = 10000;
|
||||
let num_workers = 1; // Using 1 worker to simplify effort calculation
|
||||
|
||||
@@ -23,13 +22,13 @@ fn test_average_effort_calculation() {
|
||||
&format!("Task{}", i),
|
||||
vec![],
|
||||
days_to_duration(min_days),
|
||||
days_to_duration(0.5 * (min_days + max_days)),
|
||||
days_to_duration(max_days),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let schedule =
|
||||
Schedule::new(tasks, num_workers, estimate_confidence).expect("Failed to create schedule");
|
||||
let schedule = Project::new(tasks, num_workers, None).expect("Failed to create schedule");
|
||||
|
||||
let (_, effort_times) = run_multiple_simulations(&schedule, num_simulations);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
schedule::Project,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
|
||||
fn create_schedule_and_expect_error(tasks: Vec<Task>, expected_error: &str) {
|
||||
let result = Schedule::new(tasks, 1, 0.8);
|
||||
let result = Project::new(tasks, 1, None);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
@@ -27,12 +27,14 @@ fn test_cyclic_dependency() {
|
||||
"A",
|
||||
vec!["B".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(1.5),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(1.5),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
];
|
||||
@@ -47,12 +49,14 @@ fn test_missing_dependency() {
|
||||
"A",
|
||||
vec!["C".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(1.5),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(1.5),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
];
|
||||
@@ -66,6 +70,7 @@ fn test_negative_duration() {
|
||||
"A",
|
||||
vec![],
|
||||
days_to_duration(0.0),
|
||||
days_to_duration(0.5),
|
||||
days_to_duration(1.0),
|
||||
)];
|
||||
|
||||
@@ -79,6 +84,7 @@ fn test_min_greater_than_max() {
|
||||
vec![],
|
||||
days_to_duration(3.0),
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(2.0),
|
||||
)];
|
||||
|
||||
create_schedule_and_expect_error(tasks, "Minimum duration greater than maximum");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
schedule::Project,
|
||||
simulation::run_multiple_simulations,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
@@ -13,6 +13,7 @@ fn create_independent_tasks(num_tasks: usize) -> Vec<Task> {
|
||||
&format!("Task_{}", i),
|
||||
vec![],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(3.0),
|
||||
)
|
||||
})
|
||||
@@ -23,14 +24,13 @@ fn create_independent_tasks(num_tasks: usize) -> Vec<Task> {
|
||||
fn test_no_dependencies_schedule() {
|
||||
let num_tasks = 32;
|
||||
let tasks = create_independent_tasks(num_tasks);
|
||||
let estimate_confidence = 0.8;
|
||||
let num_simulations = 1_000;
|
||||
|
||||
let mut prev_avg_effort = 0.0;
|
||||
|
||||
for num_workers in [1, 2, 4, 8] {
|
||||
let schedule = Schedule::new(tasks.clone(), num_workers, estimate_confidence)
|
||||
.expect("Failed to create schedule");
|
||||
let schedule =
|
||||
Project::new(tasks.clone(), num_workers, None).expect("Failed to create schedule");
|
||||
let (project_durations, effort_times) =
|
||||
run_multiple_simulations(&schedule, num_simulations);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
schedule::Project,
|
||||
simulation::run_multiple_simulations,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
@@ -9,47 +9,57 @@ use std::time::Duration;
|
||||
#[test]
|
||||
fn test_simulation_with_different_worker_counts() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], days_to_duration(1.0), days_to_duration(3.0)),
|
||||
Task::new(
|
||||
"A",
|
||||
vec![],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(3.0),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(3.0),
|
||||
days_to_duration(4.0),
|
||||
),
|
||||
Task::new(
|
||||
"C",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(3.0),
|
||||
days_to_duration(4.0),
|
||||
days_to_duration(5.0),
|
||||
),
|
||||
Task::new(
|
||||
"D",
|
||||
vec!["B".to_string(), "C".to_string()],
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(4.0),
|
||||
days_to_duration(6.0),
|
||||
),
|
||||
Task::new(
|
||||
"E",
|
||||
vec!["B".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(1.5),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
Task::new(
|
||||
"F",
|
||||
vec!["D".to_string(), "E".to_string()],
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(3.0),
|
||||
days_to_duration(4.0),
|
||||
),
|
||||
];
|
||||
|
||||
let estimate_confidence = 0.8;
|
||||
let num_simulations = 1000;
|
||||
|
||||
let mut all_efforts = Vec::new();
|
||||
|
||||
for num_workers in [1, 2, 4, 8, 16, 32] {
|
||||
let schedule = Schedule::new(tasks.clone(), num_workers, estimate_confidence)
|
||||
.expect("Failed to create schedule");
|
||||
let schedule =
|
||||
Project::new(tasks.clone(), num_workers, None).expect("Failed to create schedule");
|
||||
let (project_durations, effort_times) =
|
||||
run_multiple_simulations(&schedule, num_simulations);
|
||||
|
||||
|
||||
Reference in new issue
Block a user