feat: switch to Pert, add calendar scheduling

This commit is contained in:
swaits committed 2024-08-28 12:16:46 -06:00
1 parent b3207a64f5
commit 0b3b5c6896
18 files changed
+958 -323

No files matched your search

Generated
+366 -17
View File
@@ -2,6 +2,21 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.15" version = "0.6.15"
@@ -51,6 +66,15 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.3.0"
@@ -63,12 +87,33 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 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]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
dependencies = [
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@@ -76,10 +121,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "chrono"
version = "4.5.15" version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"clap_builder", "clap_builder",
] ]
@@ -108,6 +168,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.5" version = "0.8.5"
@@ -133,6 +199,17 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 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]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@@ -178,6 +255,29 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.4.0" version = "2.4.0"
@@ -201,10 +301,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "libc" name = "js-sys"
version = "0.2.155" version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "libm" name = "libm"
@@ -219,10 +328,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]] [[package]]
name = "mcps" name = "log"
version = "0.1.1" 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 = [ dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "mcps"
version = "0.2.0"
dependencies = [
"chrono",
"clap", "clap",
"distimate",
"rand", "rand",
"rand_distr", "rand_distr",
"rayon", "rayon",
@@ -230,6 +357,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"tempfile", "tempfile",
"workdays",
] ]
[[package]] [[package]]
@@ -238,6 +366,63 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -254,6 +439,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.20"
@@ -321,6 +512,12 @@ dependencies = [
"rand", "rand",
] ]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.10.0" version = "1.10.0"
@@ -361,19 +558,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]] [[package]]
name = "serde" name = "safe_arch"
version = "1.0.207" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.207" version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -382,9 +588,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.124" version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -405,6 +611,37 @@ dependencies = [
"unsafe-libyaml", "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]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -413,9 +650,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.74" version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -435,6 +672,32 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -541,6 +878,18 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 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]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"
+4 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mcps" name = "mcps"
version = "0.1.1" version = "0.2.0"
authors = ["Stephen Waits <steve@waits.net>"] authors = ["Stephen Waits <steve@waits.net>"]
edition = "2021" edition = "2021"
description = "A command-line tool for running Monte Carlo simulations on project schedules." description = "A command-line tool for running Monte Carlo simulations on project schedules."
@@ -22,13 +22,16 @@ name = "mcps"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
chrono = "0.4.38"
clap = "4.5.15" clap = "4.5.15"
distimate = "0.2.0"
rand = "0.8.5" rand = "0.8.5"
rand_distr = "0.4.3" rand_distr = "0.4.3"
rayon = "1.10.0" rayon = "1.10.0"
serde = { version = "1.0.207", features = ["derive"] } serde = { version = "1.0.207", features = ["derive"] }
serde_json = "1.0.124" serde_json = "1.0.124"
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
workdays = "0.1.0"
[dev-dependencies] [dev-dependencies]
tempfile = "3.12.0" tempfile = "3.12.0"
+58 -20
View File
@@ -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 Carlo simulations on project schedules to estimate completion times and effort
required. It helps project managers and engineers gain a better understanding of required. It helps project managers and engineers gain a better understanding of
potential project risks and timelines by providing a probabilistic analysis 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 ## Features
- **Simulate Project Schedules**: Run multiple simulations to estimate the - **Simulate Project Schedules**: Run multiple simulations to estimate the
probability distribution of project completion times and total work effort. 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 with options to override certain parameters like the number of workers or the
number of iterations. number of iterations.
- **Visual Output**: Generates an ASCII-based cumulative distribution function - **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 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 ### Option 2: Clone and build the repository locally
```bash ```bash
@@ -46,45 +49,79 @@ The binary will be available at `target/release/mcps`.
## Usage ## 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. file in YAML or JSON format.
### Basic Usage ### Basic Usage
```bash ```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 This command runs the Monte Carlo simulation on the
with 100,000 iterations and overrides the number of workers to 10. [`project.yaml`](./assets/project.yaml) project file with 100,000 iterations
and overrides the number of workers to 10.
### Command-Line Options ### Command-Line Options
- `-i, --iterations <iterations>`: Specify the number of iterations to run. Must be at least 100. Default is 50,000. - `-i, --iterations <iterations>`: Specify the number of iterations to run. Must
- `-w, --workers <num_workers>`: Override the number of workers specified in the schedule file. be at least 100. Default is 50,000.
- `-v, --version`: Print version information. - `-n, --workers <num_workers>`: Override `num_workers` specified in project file
- `-h, --help`: Print help information. - `-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 ```yaml
num_workers: 5 # for the purpose of scheduling simulation 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: tasks:
- id: DesignPhase - id: DesignPhase
min_time: 1.5 # 1.5 days estimate:
max_time: 3.5 # 3.5 days min: 1.5 # 1.5 days
likely: 2.2 # 2.2 days (best guess at actual time)
max: 3.5 # 3.5 days
dependencies: [] dependencies: []
- id: ImplementationPhase - id: ImplementationPhase
min_time: 2.25 # 2.25 days estimate:
max_time: 4.75 # 4.75 days 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] dependencies: [DesignPhase]
``` ```
This repo includes examples in [JSON](./example.json) and This repo includes examples in [JSON](./assets/project.json) and
[YAML](./example.yaml). [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 ### Example Output
@@ -111,4 +148,5 @@ always welcome!
### License ### 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.
+40
View File
@@ -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);
}
```
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 153 KiB

+66 -27
View File
@@ -1,49 +1,67 @@
{ {
"num_workers": 8, "num_workers": 8,
"estimate_confidence": 0.8, "start_date": "2024-08-01",
"tasks": [ "tasks": [
{ {
"id": "RequirementsGathering", "id": "RequirementsGathering",
"min_time": 10.0, "estimate": {
"max_time": 30.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [] "dependencies": []
}, },
{ {
"id": "SystemDesign", "id": "SystemDesign",
"min_time": 15.0, "estimate": {
"max_time": 45.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"RequirementsGathering" "RequirementsGathering"
] ]
}, },
{ {
"id": "DatabaseDesign", "id": "DatabaseDesign",
"min_time": 10.0, "estimate": {
"max_time": 35.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"SystemDesign" "SystemDesign"
] ]
}, },
{ {
"id": "UIUXDesign", "id": "UIUXDesign",
"min_time": 20.0, "estimate": {
"max_time": 50.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"RequirementsGathering" "RequirementsGathering"
] ]
}, },
{ {
"id": "FrontendDevelopment", "id": "FrontendDevelopment",
"min_time": 25.0, "estimate": {
"max_time": 60.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"UIUXDesign" "UIUXDesign"
] ]
}, },
{ {
"id": "BackendDevelopment", "id": "BackendDevelopment",
"min_time": 30.0, "estimate": {
"max_time": 80.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"SystemDesign", "SystemDesign",
"DatabaseDesign" "DatabaseDesign"
@@ -51,16 +69,22 @@
}, },
{ {
"id": "APIDevelopment", "id": "APIDevelopment",
"min_time": 20.0, "estimate": {
"max_time": 60.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"BackendDevelopment" "BackendDevelopment"
] ]
}, },
{ {
"id": "IntegrationTesting", "id": "IntegrationTesting",
"min_time": 15.0, "estimate": {
"max_time": 45.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"FrontendDevelopment", "FrontendDevelopment",
"BackendDevelopment", "BackendDevelopment",
@@ -69,32 +93,44 @@
}, },
{ {
"id": "UserAcceptanceTesting", "id": "UserAcceptanceTesting",
"min_time": 10.0, "estimate": {
"max_time": 30.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"IntegrationTesting" "IntegrationTesting"
] ]
}, },
{ {
"id": "PerformanceOptimization", "id": "PerformanceOptimization",
"min_time": 10.0, "estimate": {
"max_time": 40.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"IntegrationTesting" "IntegrationTesting"
] ]
}, },
{ {
"id": "SecurityAudit", "id": "SecurityAudit",
"min_time": 10.0, "estimate": {
"max_time": 30.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"IntegrationTesting" "IntegrationTesting"
] ]
}, },
{ {
"id": "FinalDeployment", "id": "FinalDeployment",
"min_time": 5.0, "estimate": {
"max_time": 15.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"UserAcceptanceTesting", "UserAcceptanceTesting",
"PerformanceOptimization", "PerformanceOptimization",
@@ -103,8 +139,11 @@
}, },
{ {
"id": "ProjectRetrospective", "id": "ProjectRetrospective",
"min_time": 3.0, "estimate": {
"max_time": 10.0, "min": 10.0,
"likely": 25.0,
"max": 30.0
},
"dependencies": [ "dependencies": [
"FinalDeployment" "FinalDeployment"
] ]
+145 -73
View File
@@ -1,129 +1,177 @@
num_workers: 5 num_workers: 5
estimate_confidence: 0.80 start_date: 2024-08-01
tasks: tasks:
# Project Initiation and Planning # Project Initiation and Planning
- id: project_kickoff - id: project_kickoff
min_time: 2 estimate:
max_time: 3 min: 2
likely: 2.8
max: 3
dependencies: [] dependencies: []
- id: requirements_gathering - id: requirements_gathering
min_time: 10 estimate:
max_time: 15 min: 10
likely: 13
max: 15
dependencies: [project_kickoff] dependencies: [project_kickoff]
- id: system_architecture_design - id: system_architecture_design
min_time: 15 estimate:
max_time: 20 min: 15
likely: 18
max: 20
dependencies: [requirements_gathering] dependencies: [requirements_gathering]
- id: database_design - id: database_design
min_time: 5 estimate:
max_time: 10 min: 5
likely: 8
max: 10
dependencies: [system_architecture_design] dependencies: [system_architecture_design]
# Frontend Development # Frontend Development
- id: ui_ux_design - id: ui_ux_design
min_time: 20 estimate:
max_time: 30 min: 20
likely: 24
max: 30
dependencies: [requirements_gathering] dependencies: [requirements_gathering]
- id: frontend_setup - id: frontend_setup
min_time: 3 estimate:
max_time: 5 min: 3
likely: 4.2
max: 5
dependencies: [ui_ux_design] dependencies: [ui_ux_design]
- id: user_authentication_frontend - id: user_authentication_frontend
min_time: 5 estimate:
max_time: 10 min: 5
likely: 6.1
max: 10
dependencies: [frontend_setup] dependencies: [frontend_setup]
- id: product_catalog_frontend - id: product_catalog_frontend
min_time: 15 estimate:
max_time: 25 min: 15
likely: 17
max: 25
dependencies: [frontend_setup] dependencies: [frontend_setup]
- id: shopping_cart_frontend - id: shopping_cart_frontend
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [frontend_setup] dependencies: [frontend_setup]
- id: checkout_process_frontend - id: checkout_process_frontend
min_time: 15 estimate:
max_time: 20 min: 15
likely: 17
max: 20
dependencies: [shopping_cart_frontend] dependencies: [shopping_cart_frontend]
- id: user_profile_frontend - id: user_profile_frontend
min_time: 5 estimate:
max_time: 10 min: 5
likely: 6
max: 10
dependencies: [user_authentication_frontend] dependencies: [user_authentication_frontend]
- id: search_functionality_frontend - id: search_functionality_frontend
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [product_catalog_frontend] dependencies: [product_catalog_frontend]
# Backend Development # Backend Development
- id: backend_setup - id: backend_setup
min_time: 5 estimate:
max_time: 10 min: 5
likely: 7
max: 10
dependencies: [system_architecture_design, database_design] dependencies: [system_architecture_design, database_design]
- id: user_authentication_backend - id: user_authentication_backend
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [backend_setup] dependencies: [backend_setup]
- id: product_catalog_backend - id: product_catalog_backend
min_time: 20 estimate:
max_time: 30 min: 20
likely: 25
max: 30
dependencies: [backend_setup] dependencies: [backend_setup]
- id: shopping_cart_backend - id: shopping_cart_backend
min_time: 15 estimate:
max_time: 20 min: 15
likely: 18
max: 20
dependencies: [backend_setup] dependencies: [backend_setup]
- id: checkout_process_backend - id: checkout_process_backend
min_time: 20 estimate:
max_time: 25 min: 20
likely: 23
max: 25
dependencies: [shopping_cart_backend] dependencies: [shopping_cart_backend]
- id: user_profile_backend - id: user_profile_backend
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [user_authentication_backend] dependencies: [user_authentication_backend]
- id: search_functionality_backend - id: search_functionality_backend
min_time: 15 estimate:
max_time: 20 min: 15
likely: 18
max: 20
dependencies: [product_catalog_backend] dependencies: [product_catalog_backend]
- id: payment_gateway_integration - id: payment_gateway_integration
min_time: 10 estimate:
max_time: 15 min: 10
likely: 13
max: 15
dependencies: [checkout_process_backend] dependencies: [checkout_process_backend]
# API Development and Integration # API Development and Integration
- id: api_design - id: api_design
min_time: 5 estimate:
max_time: 10 min: 5
likely: 7.5
max: 10
dependencies: [system_architecture_design] dependencies: [system_architecture_design]
- id: api_development - id: api_development
min_time: 25 estimate:
max_time: 35 min: 25
likely: 32
max: 35
dependencies: [api_design, backend_setup] dependencies: [api_design, backend_setup]
- id: api_documentation - id: api_documentation
min_time: 5 estimate:
max_time: 10 min: 5
likely: 7
max: 10
dependencies: [api_development] dependencies: [api_development]
# Testing # Testing
- id: unit_testing - id: unit_testing
min_time: 20 estimate:
max_time: 30 min: 20
likely: 25
max: 30
dependencies: dependencies:
[ [
user_authentication_backend, user_authentication_backend,
@@ -135,55 +183,75 @@ tasks:
] ]
- id: integration_testing - id: integration_testing
min_time: 15 estimate:
max_time: 20 min: 15
likely: 16
max: 20
dependencies: [unit_testing, api_development] dependencies: [unit_testing, api_development]
- id: user_acceptance_testing - id: user_acceptance_testing
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [integration_testing] dependencies: [integration_testing]
# Security and Performance # Security and Performance
- id: security_audit - id: security_audit
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [integration_testing] dependencies: [integration_testing]
- id: performance_optimization - id: performance_optimization
min_time: 15 estimate:
max_time: 20 min: 15
likely: 17
max: 20
dependencies: [integration_testing] dependencies: [integration_testing]
# Deployment and Launch # Deployment and Launch
- id: deployment_planning - id: deployment_planning
min_time: 5 estimate:
max_time: 10 min: 5
likely: 7
max: 10
dependencies: [system_architecture_design] dependencies: [system_architecture_design]
- id: staging_environment_setup - id: staging_environment_setup
min_time: 5 estimate:
max_time: 10 min: 5
likely: 7
max: 10
dependencies: [deployment_planning] dependencies: [deployment_planning]
- id: production_environment_setup - id: production_environment_setup
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [staging_environment_setup] dependencies: [staging_environment_setup]
- id: data_migration - id: data_migration
min_time: 10 estimate:
max_time: 20 min: 10
likely: 12
max: 20
dependencies: [production_environment_setup] dependencies: [production_environment_setup]
- id: user_training - id: user_training
min_time: 5 estimate:
max_time: 10 min: 5
likely: 7
max: 10
dependencies: [user_acceptance_testing] dependencies: [user_acceptance_testing]
- id: final_deployment - id: final_deployment
min_time: 5 estimate:
max_time: 10 min: 5
likely: 7
max: 10
dependencies: dependencies:
[ [
user_acceptance_testing, user_acceptance_testing,
@@ -195,11 +263,15 @@ tasks:
# Post-launch # Post-launch
- id: post_launch_support - id: post_launch_support
min_time: 10 estimate:
max_time: 15 min: 10
likely: 12
max: 15
dependencies: [final_deployment] dependencies: [final_deployment]
- id: project_retrospective - id: project_retrospective
min_time: 2 estimate:
max_time: 3 min: 2
likely: 2.8
max: 3
dependencies: [post_launch_support] dependencies: [post_launch_support]
+10
View File
@@ -0,0 +1,10 @@
{
"work_days": [
"Monday",
"Tuesday",
"Wednesday"
],
"holidays": [
"2023-12-25"
]
}
+6
View File
@@ -0,0 +1,6 @@
work_days:
- Monday
- Tuesday
- Wednesday
holidays:
- 2023-12-25
+68 -63
View File
@@ -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 clap::{Arg, Command};
use workdays::WorkCalendar;
use std::time::Duration; use std::{str::FromStr, time::Duration};
const AFTER_HELP_TEXT: &str = "\ 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>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let matches = Command::new("Monte Carlo Project Scheduler") 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") .about("Runs Monte Carlo simulations on project schedules")
.arg( .arg(
Arg::new("filename") Arg::new("filename")
.help("Path to the schedule file (.yaml or .json).") .help("Path to the project file (.yaml or .json)")
.required(true) .required(true)
.index(1), .index(1),
) )
@@ -68,15 +33,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
) )
.arg( .arg(
Arg::new("workers") Arg::new("workers")
.short('w') .short('n')
.long("workers") .long("workers")
.help("Override `num_workers` specified in schedule file") .help("Override `num_workers` specified in project file")
.value_name("num_workers"), .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) .after_help(AFTER_HELP_TEXT)
.get_matches(); .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()?; let num_simulations: usize = matches.get_one::<String>("iterations").unwrap().parse()?;
// Ensure iterations is >= 100 // 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()); return Err("Iterations must be at least 100.".into());
} }
// Load the schedule // Load the project
let mut schedule = Schedule::from_file(schedule_path)?; 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 // Check if workers are overridden by command-line argument
if let Some(workers_str) = matches.get_one::<String>("workers") { 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 { if workers < 1 {
return Err("Invalid number of workers, must be 1 or more".into()); return Err("Invalid number of workers, must be 1 or more".into());
} }
schedule.num_workers = workers; project.num_workers = workers;
} }
// Monte Carlo simulation // 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 // Results output
print_ascii_cdf( print_ascii_cdf(
&project_durations, &project_durations,
format!( format!(
"Project Completion Time with {} Worker{} (by probability)", "Completion Time ({} Worker{}, starting {})",
schedule.num_workers, project.num_workers,
if schedule.num_workers == 1 { "" } else { "s" } if project.num_workers == 1 { "" } else { "s" },
start_date,
) )
.as_str(), .as_str(),
"Duration", &start_date,
&calendar,
); );
println!(); println!();
print_ascii_cdf( print_ascii_cdf(
&effort_times, &effort_times,
"Total Work Effort (by probability)", format!("Total Work Effort (1 worker, starting {})", start_date).as_str(),
"Effort", &start_date,
&calendar,
); );
Ok(()) 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(); let mut sorted_data = data.to_vec();
sorted_data.sort_unstable(); sorted_data.sort_unstable();
let min = sorted_data[0]; 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() padding_right = width - padding - title.len()
); );
println!("────┬────────────────────────────────────────────────────────────┬──────────"); println!("────┬────────────────────────────────────────────────────────────┬──────────┬──────────┬──────────");
println!("%ile│{}│{:>10}", centered_title, units); println!("%ile│{}│ Workdays │ Schedule │ Complete ", centered_title);
println!("────┼────────────────────────────────────────────────────────────┼──────────"); println!("────┼────────────────────────────────────────────────────────────┼──────────┼──────────┼──────────");
let bar_width = 60; 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; let offset = (bar_width - (max_bar_position - min_bar_position)) / 2;
for (i, (bar_position, days)) in bar_positions.iter().enumerate() { 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 shifted_bar_position = bar_position + offset;
let (fg, bg) = if i % 2 == 0 { 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 0..=50 => "\x1b[31m", // Red
55..=75 => "\x1b[33m", // Orange 55..=75 => "\x1b[33m", // Orange
80..=90 => "\x1b[32m", // Green 80..=90 => "\x1b[32m", // Green
95..=100 => "\x1b[31m", // Red 95..=100 => "\x1b[33m", // Orange
_ => "\x1b[0m", // Default (shouldn't happen) _ => "\x1b[0m", // Default (shouldn't happen)
}; };
let reset_code = "\x1b[0m"; let reset_code = "\x1b[0m";
@@ -205,7 +204,7 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
.collect(); .collect();
println!( println!(
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}", "{}{:>4}{}│{}{}{}│{}{:5.0} days{}│{}{:5.0} days{}│{}{}{}",
color_code, color_code,
format!("p{}", 100 - i * 5), format!("p{}", 100 - i * 5),
reset_code, reset_code,
@@ -214,8 +213,14 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
reset_code, reset_code,
color_code, color_code,
days, days,
reset_code,
color_code,
calendar_duration.num_days(),
reset_code,
color_code,
end_date,
reset_code reset_code
); );
} }
println!("────┴────────────────────────────────────────────────────────────┴──────────"); println!("────┴────────────────────────────────────────────────────────────┴──────────┴──────────┴──────────");
} }
+41 -38
View File
@@ -1,25 +1,27 @@
use crate::task::Task; use crate::task::Task;
use chrono::NaiveDate;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::time::Duration; use std::time::Duration;
#[derive(Debug)] #[derive(Debug)]
pub struct Schedule { pub struct Project {
pub tasks: Vec<Task>, pub tasks: Vec<Task>,
pub num_workers: usize, pub num_workers: usize,
pub estimate_confidence: f64, pub start_date: Option<NaiveDate>,
} }
impl Schedule { impl Project {
pub fn new( pub fn new(
tasks: Vec<Task>, tasks: Vec<Task>,
num_workers: usize, num_workers: usize,
estimate_confidence: f64, start_date: Option<NaiveDate>,
) -> Result<Self, String> { ) -> Result<Self, String> {
let schedule = Schedule { let schedule = Project {
tasks, tasks,
num_workers, num_workers,
estimate_confidence, start_date,
}; };
schedule.validate()?; schedule.validate()?;
Ok(schedule) Ok(schedule)
@@ -94,11 +96,12 @@ impl Schedule {
mod tests { mod tests {
use super::*; 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 { Task {
id: id.to_string(), id: id.to_string(),
dependencies: deps.into_iter().map(String::from).collect(), dependencies: deps.into_iter().map(String::from).collect(),
min_time: Duration::from_secs(min), min_time: Duration::from_secs(min),
likely_time: Duration::from_secs(likely),
max_time: Duration::from_secs(max), max_time: Duration::from_secs(max),
} }
} }
@@ -106,25 +109,25 @@ mod tests {
#[test] #[test]
fn test_valid_schedule() { fn test_valid_schedule() {
let tasks = vec![ let tasks = vec![
create_task("A", 1, 3, vec![]), create_task("A", 1, 2, 3, vec![]),
create_task("B", 2, 4, vec!["A"]), create_task("B", 2, 3, 4, vec!["A"]),
create_task("C", 3, 5, 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()); assert!(schedule.is_ok());
} }
#[test] #[test]
fn test_empty_task_list() { 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!(schedule.is_err());
assert_eq!(schedule.unwrap_err(), "Empty task list"); assert_eq!(schedule.unwrap_err(), "Empty task list");
} }
#[test] #[test]
fn test_invalid_task_duration() { fn test_invalid_task_duration() {
let tasks = vec![create_task("A", 3, 1, vec![])]; let tasks = vec![create_task("A", 3, 2, 1, vec![])];
let schedule = Schedule::new(tasks, 2, 0.8); let schedule = Project::new(tasks, 2, None);
assert!(schedule.is_err()); assert!(schedule.is_err());
assert_eq!( assert_eq!(
schedule.unwrap_err(), schedule.unwrap_err(),
@@ -134,8 +137,8 @@ mod tests {
#[test] #[test]
fn test_zero_duration() { fn test_zero_duration() {
let tasks = vec![create_task("A", 0, 1, vec![])]; let tasks = vec![create_task("A", 0, 1, 2, vec![])];
let schedule = Schedule::new(tasks, 2, 0.8); let schedule = Project::new(tasks, 2, None);
assert!(schedule.is_err()); assert!(schedule.is_err());
assert_eq!(schedule.unwrap_err(), "Invalid task duration for task A"); assert_eq!(schedule.unwrap_err(), "Invalid task duration for task A");
} }
@@ -143,10 +146,10 @@ mod tests {
#[test] #[test]
fn test_missing_dependency() { fn test_missing_dependency() {
let tasks = vec![ let tasks = vec![
create_task("A", 1, 3, vec![]), create_task("A", 1, 2, 3, vec![]),
create_task("B", 2, 4, vec!["C"]), 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!(schedule.is_err());
assert_eq!(schedule.unwrap_err(), "Missing dependency C for task B"); assert_eq!(schedule.unwrap_err(), "Missing dependency C for task B");
} }
@@ -154,10 +157,10 @@ mod tests {
#[test] #[test]
fn test_cyclic_dependency() { fn test_cyclic_dependency() {
let tasks = vec![ let tasks = vec![
create_task("A", 1, 3, vec!["B"]), create_task("A", 1, 2, 3, vec!["B"]),
create_task("B", 2, 4, vec!["A"]), 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.is_err());
assert!(schedule assert!(schedule
.unwrap_err() .unwrap_err()
@@ -167,28 +170,28 @@ mod tests {
#[test] #[test]
fn test_complex_valid_schedule() { fn test_complex_valid_schedule() {
let tasks = vec![ let tasks = vec![
create_task("A", 1, 3, vec![]), create_task("A", 1, 2, 3, vec![]),
create_task("B", 2, 4, vec!["A"]), create_task("B", 2, 3, 4, vec!["A"]),
create_task("C", 3, 5, vec!["A"]), create_task("C", 3, 4, 5, vec!["A"]),
create_task("D", 2, 6, vec!["B", "C"]), create_task("D", 2, 4, 6, vec!["B", "C"]),
create_task("E", 1, 2, vec!["A"]), create_task("E", 1, 2, 4, vec!["A"]),
create_task("F", 2, 4, vec!["D", "E"]), 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()); assert!(schedule.is_ok());
} }
#[test] #[test]
fn test_complex_cyclic_dependency() { fn test_complex_cyclic_dependency() {
let tasks = vec![ let tasks = vec![
create_task("A", 1, 3, vec!["F"]), create_task("A", 1, 2, 3, vec!["F"]),
create_task("B", 2, 4, vec!["A"]), create_task("B", 2, 3, 4, vec!["A"]),
create_task("C", 3, 5, vec!["B"]), create_task("C", 3, 4, 5, vec!["B"]),
create_task("D", 2, 6, vec!["C"]), create_task("D", 2, 3, 6, vec!["C"]),
create_task("E", 1, 2, vec!["D"]), create_task("E", 1, 2, 3, vec!["D"]),
create_task("F", 2, 4, vec!["E"]), create_task("F", 2, 3, 4, vec!["E"]),
]; ];
let schedule = Schedule::new(tasks, 3, 0.9); let schedule = Project::new(tasks, 3, None);
assert!( assert!(
schedule.is_err(), schedule.is_err(),
"Expected cyclic dependency error, but got Ok" "Expected cyclic dependency error, but got Ok"
@@ -204,8 +207,8 @@ mod tests {
#[test] #[test]
fn test_self_dependency() { fn test_self_dependency() {
let tasks = vec![create_task("A", 1, 3, vec!["A"])]; let tasks = vec![create_task("A", 1, 2, 3, vec!["A"])];
let schedule = Schedule::new(tasks, 1, 0.8); let schedule = Project::new(tasks, 1, None);
assert!( assert!(
schedule.is_err(), schedule.is_err(),
"Expected self-dependency error, but got Ok" "Expected self-dependency error, but got Ok"
+55 -35
View File
@@ -1,5 +1,6 @@
use crate::{schedule::Schedule, task::Task}; use crate::{schedule::Project, task::Task};
use chrono::NaiveDate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fs::File, io::Read, path::Path, time::Duration}; 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)] #[derive(Debug, Deserialize, Serialize)]
struct ScheduleInput { struct ScheduleInput {
num_workers: usize, num_workers: usize,
estimate_confidence: f64, start_date: Option<NaiveDate>,
tasks: Vec<TaskInput>, tasks: Vec<TaskInput>,
} }
#[derive(Debug, Deserialize, Serialize)]
struct EstimateInput {
min: f64,
max: f64,
likely: f64,
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct TaskInput { struct TaskInput {
id: String, id: String,
min_time: f64, name: Option<String>,
max_time: f64, description: Option<String>,
estimate: EstimateInput,
dependencies: Vec<String>, dependencies: Vec<String>,
} }
@@ -24,7 +33,7 @@ enum FileFormat {
Json, Json,
} }
impl Schedule { impl Project {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> { pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
let mut file = File::open(&path)?; let mut file = File::open(&path)?;
let mut contents = String::new(); let mut contents = String::new();
@@ -41,13 +50,14 @@ impl Schedule {
.into_iter() .into_iter()
.map(|t| Task { .map(|t| Task {
id: t.id, id: t.id,
min_time: Duration::from_secs_f64(t.min_time * 24.0 * 60.0 * 60.0), min_time: Duration::from_secs_f64(t.estimate.min * 24.0 * 60.0 * 60.0),
max_time: Duration::from_secs_f64(t.max_time * 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, dependencies: t.dependencies,
}) })
.collect(); .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()?; schedule.validate()?;
@@ -89,22 +99,24 @@ mod tests {
fn test_load_from_yaml() { fn test_load_from_yaml() {
let yaml_content = r#" let yaml_content = r#"
num_workers: 3 num_workers: 3
estimate_confidence: 0.9
tasks: tasks:
- id: A - id: A
min_time: 1 estimate:
max_time: 3 min: 1
likely: 2.4
max: 3
dependencies: [] dependencies: []
- id: B - id: B
min_time: 2 estimate:
max_time: 4 min: 2
likely: 2.4
max: 4
dependencies: [A] dependencies: [A]
"#; "#;
let (_temp_file, path) = create_temp_file(yaml_content, "yaml"); 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.tasks.len(), 2);
assert_eq!(schedule.num_workers, 3); assert_eq!(schedule.num_workers, 3);
assert_eq!(schedule.estimate_confidence, 0.9);
} }
#[test] #[test]
@@ -112,35 +124,39 @@ tasks:
let json_content = r#" let json_content = r#"
{ {
"num_workers": 3, "num_workers": 3,
"estimate_confidence": 0.9,
"tasks": [ "tasks": [
{ {
"id": "A", "id": "A",
"min_time": 1, "estimate": {
"max_time": 3, "min": 1,
"likely": 1.8,
"max": 3
},
"dependencies": [] "dependencies": []
}, },
{ {
"id": "B", "id": "B",
"min_time": 2, "estimate": {
"max_time": 4, "min": 2,
"likely": 1.8,
"max": 4
},
"dependencies": ["A"] "dependencies": ["A"]
} }
] ]
} }
"#; "#;
let (_temp_file, path) = create_temp_file(json_content, "json"); 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.tasks.len(), 2);
assert_eq!(schedule.num_workers, 3); assert_eq!(schedule.num_workers, 3);
assert_eq!(schedule.estimate_confidence, 0.9);
} }
#[test] #[test]
fn test_unsupported_format() { fn test_unsupported_format() {
let content = "irrelevant content"; let content = "irrelevant content";
let (_temp_file, path) = create_temp_file(content, "txt"); 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.is_err());
assert!(result assert!(result
.unwrap_err() .unwrap_err()
@@ -152,15 +168,16 @@ tasks:
fn test_invalid_schedule() { fn test_invalid_schedule() {
let yaml_content = r#" let yaml_content = r#"
num_workers: 3 num_workers: 3
estimate_confidence: 0.9
tasks: tasks:
- id: A - id: A
min_time: 3 estimate:
max_time: 1 # Invalid: min_time > max_time min: 3
likely: 2
max: 1 # Invalid: min_time > max_time
dependencies: [] dependencies: []
"#; "#;
let (_temp_file, path) = create_temp_file(yaml_content, "yaml"); 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.is_err());
assert!(result assert!(result
.unwrap_err() .unwrap_err()
@@ -172,15 +189,16 @@ tasks:
fn test_load_with_floating_point_times_yaml() { fn test_load_with_floating_point_times_yaml() {
let yaml_content = r#" let yaml_content = r#"
num_workers: 3 num_workers: 3
estimate_confidence: 0.9
tasks: tasks:
- id: A - id: A
min_time: 1.5 # 1.5 days estimate:
max_time: 3.5 # 3.5 days min: 1.5 # 1.5 days
likely: 2 # 2.0 days
max: 3.5 # 3.5 days
dependencies: [] dependencies: []
"#; "#;
let (_temp_file, path) = create_temp_file(yaml_content, "yaml"); 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 // Verify that there is one task and it has the correct times
assert_eq!(schedule.tasks.len(), 1); assert_eq!(schedule.tasks.len(), 1);
@@ -199,19 +217,21 @@ tasks:
let json_content = r#" let json_content = r#"
{ {
"num_workers": 3, "num_workers": 3,
"estimate_confidence": 0.9,
"tasks": [ "tasks": [
{ {
"id": "A", "id": "A",
"min_time": 1.5, "estimate": {
"max_time": 3.5, "min": 1.5,
"likely": 3,
"max": 3.5
},
"dependencies": [] "dependencies": []
} }
] ]
} }
"#; "#;
let (_temp_file, path) = create_temp_file(json_content, "json"); 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 // Verify that there is one task and it has the correct times
assert_eq!(schedule.tasks.len(), 1); assert_eq!(schedule.tasks.len(), 1);
+62 -34
View File
@@ -1,7 +1,7 @@
use crate::schedule::Schedule; use crate::schedule::Project;
use distimate::Pert;
use rand::prelude::*; use rand::prelude::*;
use rand_distr::{Distribution, Normal};
use rayon::prelude::*; use rayon::prelude::*;
use std::{ use std::{
@@ -9,13 +9,14 @@ use std::{
time::Duration, time::Duration,
}; };
#[derive(Debug)]
pub struct SimulationResult { pub struct SimulationResult {
pub total_project_duration: Duration, pub total_project_duration: Duration,
pub total_effort_time: Duration, pub total_effort_time: Duration,
} }
pub fn run_multiple_simulations( pub fn run_multiple_simulations(
schedule: &Schedule, schedule: &Project,
num_simulations: usize, num_simulations: usize,
) -> (Vec<Duration>, Vec<Duration>) { ) -> (Vec<Duration>, Vec<Duration>) {
(0..num_simulations) (0..num_simulations)
@@ -27,7 +28,7 @@ pub fn run_multiple_simulations(
.unzip() .unzip()
} }
fn run_simulation(schedule: &Schedule) -> SimulationResult { fn run_simulation(schedule: &Project) -> SimulationResult {
let mut rng = thread_rng(); let mut rng = thread_rng();
// Simulate task times // Simulate task times
@@ -35,12 +36,7 @@ fn run_simulation(schedule: &Schedule) -> SimulationResult {
.tasks .tasks
.iter() .iter()
.map(|task| { .map(|task| {
let task_time = simulate_task_time( let task_time = simulate_task_time(&mut rng, task.min_time, task.max_time);
&mut rng,
task.min_time,
task.max_time,
schedule.estimate_confidence,
);
(&task.id, task_time) (&task.id, task_time)
}) })
.collect(); .collect();
@@ -117,21 +113,13 @@ fn run_simulation(schedule: &Schedule) -> SimulationResult {
} }
} }
fn simulate_task_time( fn simulate_task_time(rng: &mut impl Rng, min_time: Duration, max_time: Duration) -> Duration {
rng: &mut impl Rng,
min_time: Duration,
max_time: Duration,
estimate_confidence: f64,
) -> Duration {
let min_secs = min_time.as_secs_f64(); let min_secs = min_time.as_secs_f64();
let max_secs = max_time.as_secs_f64(); let max_secs = max_time.as_secs_f64();
let mean = (min_secs + max_secs) / 2.0; let mean = (min_secs + max_secs) / 2.0;
let z_score = (1.0 - (1.0 - estimate_confidence) / 2.0).sqrt() * 2.0; let pert = Pert::new(min_secs, mean, max_secs).unwrap();
let std_dev = (max_secs - min_secs) / (2.0 * z_score); let sampled_secs = pert.sample(rng);
let normal = Normal::new(mean, std_dev).unwrap();
let sampled_secs = normal.sample(rng).clamp(min_secs, max_secs);
Duration::from_secs_f64(sampled_secs) Duration::from_secs_f64(sampled_secs)
} }
@@ -146,10 +134,9 @@ mod tests {
let mut rng = thread_rng(); let mut rng = thread_rng();
let min_time = Duration::from_secs(5); let min_time = Duration::from_secs(5);
let max_time = Duration::from_secs(15); let max_time = Duration::from_secs(15);
let estimate_confidence = 0.8;
for _ in 0..1000 { 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!( assert!(
time >= min_time && time <= max_time, time >= min_time && time <= max_time,
"Simulated time should be within range" "Simulated time should be within range"
@@ -159,8 +146,14 @@ mod tests {
#[test] #[test]
fn test_single_task_simulation() { fn test_single_task_simulation() {
let task = Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(15)); let task = Task::new(
let schedule = Schedule::new(vec![task], 1, 0.8).unwrap(); "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); let result = run_simulation(&schedule);
@@ -178,11 +171,29 @@ mod tests {
#[test] #[test]
fn test_multiple_independent_tasks() { fn test_multiple_independent_tasks() {
let tasks = vec![ let tasks = vec![
Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(10)), Task::new(
Task::new("B", vec![], Duration::from_secs(7), Duration::from_secs(12)), "A",
Task::new("C", vec![], Duration::from_secs(3), Duration::from_secs(8)), 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); let result = run_simulation(&schedule);
@@ -199,32 +210,42 @@ mod tests {
#[test] #[test]
fn test_tasks_with_dependencies() { fn test_tasks_with_dependencies() {
let tasks = vec![ 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( Task::new(
"B", "B",
vec!["A".to_string()], vec!["A".to_string()],
Duration::from_secs(7), Duration::from_secs(7),
Duration::from_secs(9),
Duration::from_secs(12), Duration::from_secs(12),
), ),
Task::new( Task::new(
"C", "C",
vec!["A".to_string()], vec!["A".to_string()],
Duration::from_secs(3), Duration::from_secs(3),
Duration::from_secs(6),
Duration::from_secs(8), Duration::from_secs(8),
), ),
Task::new( Task::new(
"D", "D",
vec!["B".to_string(), "C".to_string()], vec!["B".to_string(), "C".to_string()],
Duration::from_secs(4), Duration::from_secs(4),
Duration::from_secs(7),
Duration::from_secs(9), 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); let result = run_simulation(&schedule);
println!("{:?}", result);
assert!( 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" "Project duration should be at least the minimum critical path duration"
); );
} }
@@ -232,15 +253,22 @@ mod tests {
#[test] #[test]
fn test_multiple_simulations_consistency() { fn test_multiple_simulations_consistency() {
let tasks = vec![ 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( Task::new(
"B", "B",
vec!["A".to_string()], vec!["A".to_string()],
Duration::from_secs(7), Duration::from_secs(7),
Duration::from_secs(10),
Duration::from_secs(12), 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); let (durations, efforts) = run_multiple_simulations(&schedule, 1000);
+7
View File
@@ -5,6 +5,7 @@ pub struct Task {
pub id: String, pub id: String,
pub dependencies: Vec<String>, pub dependencies: Vec<String>,
pub min_time: Duration, pub min_time: Duration,
pub likely_time: Duration,
pub max_time: Duration, pub max_time: Duration,
} }
@@ -13,12 +14,14 @@ impl Task {
id: &str, id: &str,
dependencies: Vec<String>, dependencies: Vec<String>,
min_time: Duration, min_time: Duration,
likely_time: Duration,
max_time: Duration, max_time: Duration,
) -> Self { ) -> Self {
Task { Task {
id: id.to_string(), id: id.to_string(),
dependencies, dependencies,
min_time, min_time,
likely_time,
max_time, max_time,
} }
} }
@@ -51,6 +54,7 @@ mod tests {
"Task1", "Task1",
vec!["Dep1".to_string(), "Dep2".to_string()], vec!["Dep1".to_string(), "Dep2".to_string()],
Duration::from_secs(1), Duration::from_secs(1),
Duration::from_secs(3),
Duration::from_secs(5), Duration::from_secs(5),
); );
@@ -66,6 +70,7 @@ mod tests {
"Task2", "Task2",
vec![], vec![],
Duration::from_secs(2), Duration::from_secs(2),
Duration::from_secs(7),
Duration::from_secs(10), Duration::from_secs(10),
); );
@@ -82,6 +87,7 @@ mod tests {
vec!["Dep3".to_string()], vec!["Dep3".to_string()],
Duration::from_secs(5), Duration::from_secs(5),
Duration::from_secs(5), Duration::from_secs(5),
Duration::from_secs(5),
); );
assert_eq!(task.id, "Task3"); assert_eq!(task.id, "Task3");
@@ -95,6 +101,7 @@ mod tests {
"Task4", "Task4",
vec!["Dep4".to_string()], vec!["Dep4".to_string()],
Duration::from_secs(3), Duration::from_secs(3),
Duration::from_secs(5),
Duration::from_secs(7), Duration::from_secs(7),
); );
let task2 = task1.clone(); let task2 = task1.clone();
+3 -4
View File
@@ -1,5 +1,5 @@
use mcps::{ use mcps::{
schedule::Schedule, schedule::Project,
simulation::run_multiple_simulations, simulation::run_multiple_simulations,
task::{days_to_duration, Task}, task::{days_to_duration, Task},
}; };
@@ -12,7 +12,6 @@ fn test_average_effort_calculation() {
let min_days = 5.0; let min_days = 5.0;
let max_days = 15.0; let max_days = 15.0;
let expected_avg_days = 10.0; let expected_avg_days = 10.0;
let estimate_confidence = 0.8;
let num_simulations = 10000; let num_simulations = 10000;
let num_workers = 1; // Using 1 worker to simplify effort calculation let num_workers = 1; // Using 1 worker to simplify effort calculation
@@ -23,13 +22,13 @@ fn test_average_effort_calculation() {
&format!("Task{}", i), &format!("Task{}", i),
vec![], vec![],
days_to_duration(min_days), days_to_duration(min_days),
days_to_duration(0.5 * (min_days + max_days)),
days_to_duration(max_days), days_to_duration(max_days),
) )
}) })
.collect(); .collect();
let schedule = let schedule = Project::new(tasks, num_workers, None).expect("Failed to create schedule");
Schedule::new(tasks, num_workers, estimate_confidence).expect("Failed to create schedule");
let (_, effort_times) = run_multiple_simulations(&schedule, num_simulations); let (_, effort_times) = run_multiple_simulations(&schedule, num_simulations);
+8 -2
View File
@@ -1,10 +1,10 @@
use mcps::{ use mcps::{
schedule::Schedule, schedule::Project,
task::{days_to_duration, Task}, task::{days_to_duration, Task},
}; };
fn create_schedule_and_expect_error(tasks: Vec<Task>, expected_error: &str) { 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!( assert!(
result.is_err(), result.is_err(),
@@ -27,12 +27,14 @@ fn test_cyclic_dependency() {
"A", "A",
vec!["B".to_string()], vec!["B".to_string()],
days_to_duration(1.0), days_to_duration(1.0),
days_to_duration(1.5),
days_to_duration(2.0), days_to_duration(2.0),
), ),
Task::new( Task::new(
"B", "B",
vec!["A".to_string()], vec!["A".to_string()],
days_to_duration(1.0), days_to_duration(1.0),
days_to_duration(1.5),
days_to_duration(2.0), days_to_duration(2.0),
), ),
]; ];
@@ -47,12 +49,14 @@ fn test_missing_dependency() {
"A", "A",
vec!["C".to_string()], vec!["C".to_string()],
days_to_duration(1.0), days_to_duration(1.0),
days_to_duration(1.5),
days_to_duration(2.0), days_to_duration(2.0),
), ),
Task::new( Task::new(
"B", "B",
vec!["A".to_string()], vec!["A".to_string()],
days_to_duration(1.0), days_to_duration(1.0),
days_to_duration(1.5),
days_to_duration(2.0), days_to_duration(2.0),
), ),
]; ];
@@ -66,6 +70,7 @@ fn test_negative_duration() {
"A", "A",
vec![], vec![],
days_to_duration(0.0), days_to_duration(0.0),
days_to_duration(0.5),
days_to_duration(1.0), days_to_duration(1.0),
)]; )];
@@ -79,6 +84,7 @@ fn test_min_greater_than_max() {
vec![], vec![],
days_to_duration(3.0), days_to_duration(3.0),
days_to_duration(2.0), days_to_duration(2.0),
days_to_duration(2.0),
)]; )];
create_schedule_and_expect_error(tasks, "Minimum duration greater than maximum"); create_schedule_and_expect_error(tasks, "Minimum duration greater than maximum");
+4 -4
View File
@@ -1,5 +1,5 @@
use mcps::{ use mcps::{
schedule::Schedule, schedule::Project,
simulation::run_multiple_simulations, simulation::run_multiple_simulations,
task::{days_to_duration, Task}, task::{days_to_duration, Task},
}; };
@@ -13,6 +13,7 @@ fn create_independent_tasks(num_tasks: usize) -> Vec<Task> {
&format!("Task_{}", i), &format!("Task_{}", i),
vec![], vec![],
days_to_duration(1.0), days_to_duration(1.0),
days_to_duration(2.0),
days_to_duration(3.0), days_to_duration(3.0),
) )
}) })
@@ -23,14 +24,13 @@ fn create_independent_tasks(num_tasks: usize) -> Vec<Task> {
fn test_no_dependencies_schedule() { fn test_no_dependencies_schedule() {
let num_tasks = 32; let num_tasks = 32;
let tasks = create_independent_tasks(num_tasks); let tasks = create_independent_tasks(num_tasks);
let estimate_confidence = 0.8;
let num_simulations = 1_000; let num_simulations = 1_000;
let mut prev_avg_effort = 0.0; let mut prev_avg_effort = 0.0;
for num_workers in [1, 2, 4, 8] { for num_workers in [1, 2, 4, 8] {
let schedule = Schedule::new(tasks.clone(), num_workers, estimate_confidence) let schedule =
.expect("Failed to create schedule"); Project::new(tasks.clone(), num_workers, None).expect("Failed to create schedule");
let (project_durations, effort_times) = let (project_durations, effort_times) =
run_multiple_simulations(&schedule, num_simulations); run_multiple_simulations(&schedule, num_simulations);
+15 -5
View File
@@ -1,5 +1,5 @@
use mcps::{ use mcps::{
schedule::Schedule, schedule::Project,
simulation::run_multiple_simulations, simulation::run_multiple_simulations,
task::{days_to_duration, Task}, task::{days_to_duration, Task},
}; };
@@ -9,47 +9,57 @@ use std::time::Duration;
#[test] #[test]
fn test_simulation_with_different_worker_counts() { fn test_simulation_with_different_worker_counts() {
let tasks = vec![ 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( Task::new(
"B", "B",
vec!["A".to_string()], vec!["A".to_string()],
days_to_duration(2.0), days_to_duration(2.0),
days_to_duration(3.0),
days_to_duration(4.0), days_to_duration(4.0),
), ),
Task::new( Task::new(
"C", "C",
vec!["A".to_string()], vec!["A".to_string()],
days_to_duration(3.0), days_to_duration(3.0),
days_to_duration(4.0),
days_to_duration(5.0), days_to_duration(5.0),
), ),
Task::new( Task::new(
"D", "D",
vec!["B".to_string(), "C".to_string()], vec!["B".to_string(), "C".to_string()],
days_to_duration(2.0), days_to_duration(2.0),
days_to_duration(4.0),
days_to_duration(6.0), days_to_duration(6.0),
), ),
Task::new( Task::new(
"E", "E",
vec!["B".to_string()], vec!["B".to_string()],
days_to_duration(1.0), days_to_duration(1.0),
days_to_duration(1.5),
days_to_duration(2.0), days_to_duration(2.0),
), ),
Task::new( Task::new(
"F", "F",
vec!["D".to_string(), "E".to_string()], vec!["D".to_string(), "E".to_string()],
days_to_duration(2.0), days_to_duration(2.0),
days_to_duration(3.0),
days_to_duration(4.0), days_to_duration(4.0),
), ),
]; ];
let estimate_confidence = 0.8;
let num_simulations = 1000; let num_simulations = 1000;
let mut all_efforts = Vec::new(); let mut all_efforts = Vec::new();
for num_workers in [1, 2, 4, 8, 16, 32] { for num_workers in [1, 2, 4, 8, 16, 32] {
let schedule = Schedule::new(tasks.clone(), num_workers, estimate_confidence) let schedule =
.expect("Failed to create schedule"); Project::new(tasks.clone(), num_workers, None).expect("Failed to create schedule");
let (project_durations, effort_times) = let (project_durations, effort_times) =
run_multiple_simulations(&schedule, num_simulations); run_multiple_simulations(&schedule, num_simulations);