Compare commits

...
13 Commits
Author SHA1 Message Date
swaits 5ab2c8928e chore: repoint URLs at git.swaits.com and bump for release
Rewrites remaining github.com / git.sr.ht references to git.swaits.com,
translating GitHub URL shapes to Gitea's (/blob/<ref>/ -> /src/branch/<ref>/,
/tree/<tag> -> /src/tag/<tag>, /actions/workflows/X -> /actions?workflow=X,
/edit/ -> /_edit/).

Left pointing at GitHub on purpose: pull-request links (PRs were not migrated)
and GitHub-only surfaces Gitea lacks (security advisories, discussions).
2026-08-31 17:58:35 -06:00
swaits 02505d4d12 chore: point repository at git.swaits.com 2026-08-31 17:19:21 -06:00
swaits 3d0efdc1d7 chore: remove Sourcehut CI manifest
Migrated to git.swaits.com; Sourcehut builds no longer run.
2026-08-31 17:16:30 -06:00
swaits 5eb7ed9dce feat: highlight 90% CI and tweak colors 2024-08-30 14:54:13 -06:00
swaits d859e505a3 chore: update deps 2024-08-28 16:47:49 -06:00
swaits 3cb2e9aaef chore: bump crate version 2024-08-28 16:39:51 -06:00
swaits f056d845e9 fix: remove copypasta in build script 2024-08-28 16:34:52 -06:00
swaits 51088435aa chore: update deny.toml and MSRV 2024-08-28 16:30:46 -06:00
swaits a22cc3a672 chore: add deny.toml for cargo deny 2024-08-28 16:18:02 -06:00
swaits 1e35223bd7 chore: setup sourcehut autobuild 2024-08-28 16:13:21 -06:00
swaits 55aac2d882 doc: update TODO 2024-08-28 16:13:05 -06:00
swaits 3195d39547 fix: update deps, align version to crate 2024-08-28 12:20:52 -06:00
swaits 0b3b5c6896 feat: switch to Pert, add calendar scheduling 2024-08-28 12:16:46 -06:00
20 changed files with 1185 additions and 351 deletions

No files matched your search

-11
View File
@@ -1,11 +0,0 @@
image: alpine/edge
secrets:
- 88cce185-d2ec-4573-9546-e33f36f79bbf
sources:
- git+ssh://git@git.sr.ht/~swaits/mcps
tasks:
- mirror-to-github: |
cd ~/mcps
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
git remote add github git@github.com:swaits/mcps.git
git push --mirror github
Generated
+372 -24
View File
@@ -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.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
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.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c93ea4f00253fe5c1fece7e0414fa6b56e59c433f52c6648752ac7eac94d9d4a"
dependencies = [
"rand",
"statrs",
"thiserror",
]
[[package]]
name = "either"
version = "1.13.0"
@@ -157,9 +234,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "getrandom"
@@ -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,17 +328,35 @@ 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.3.0"
dependencies = [
"chrono",
"clap",
"distimate",
"rand",
"rand_distr",
"rayon",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"workdays",
]
[[package]]
@@ -238,6 +365,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 +438,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"
@@ -274,9 +464,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.36"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -321,6 +511,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"
@@ -343,9 +539,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.34"
version = "0.38.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
dependencies = [
"bitflags",
"errno",
@@ -361,19 +557,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.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.207"
version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [
"proc-macro2",
"quote",
@@ -382,9 +587,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.124"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [
"itoa",
"memchr",
@@ -405,6 +610,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 +649,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.74"
version = "2.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
dependencies = [
"proc-macro2",
"quote",
@@ -435,6 +671,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 +721,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 +877,18 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "workdays"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8ef4c004ec2e8532e8c3828620ddce6253474368edfce8fd5cad2996e706baf"
dependencies = [
"chrono",
"serde",
"serde_json",
"serde_yaml",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
+7 -4
View File
@@ -1,15 +1,16 @@
[package]
name = "mcps"
version = "0.1.1"
version = "0.3.1"
authors = ["Stephen Waits <steve@waits.net>"]
edition = "2021"
description = "A command-line tool for running Monte Carlo simulations on project schedules."
license = "MIT"
readme = "README.md"
repository = "https://github.com/swaits/mcps"
homepage = "https://github.com/swaits/mcps"
repository = "https://git.swaits.com/swaits/mcps"
homepage = "https://git.swaits.com/swaits/mcps"
keywords = ["monte", "carlo", "project", "schedules", "estimation"]
categories = ["command-line-utilities", "development-tools", "simulation"]
rust-version = "1.74.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -22,13 +23,15 @@ name = "mcps"
path = "src/main.rs"
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
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"
+59 -21
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
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,10 +34,13 @@ 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
git clone https://github.com/swaits/mcps.git
git clone https://git.swaits.com/swaits/mcps.git
cd mcps
cargo build --release
```
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
# TODO
- add CSV output of distributions
- break project out into its own crate
- add sourcehut autobuild
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 165 KiB

+66 -27
View File
@@ -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"
]
+145 -73
View File
@@ -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]
+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
+231
View File
@@ -0,0 +1,231 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "Unicode-DFS-2016"]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The package spec the clarification applies to
#crate = "ring"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = []
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
bitbucket = []
+89 -69
View File
@@ -1,61 +1,26 @@
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://git.swaits.com/swaits/mcps/src/branch/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")
.version("0.1.1")
.version("0.3.0")
.author("Stephen Waits <steve@waits.net>")
.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,21 @@ 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 +85,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 +136,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 +176,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 {
@@ -188,11 +188,12 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
};
let color_code = match 100 - i * 5 {
0..=50 => "\x1b[31m", // Red
55..=75 => "\x1b[33m", // Orange
80..=90 => "\x1b[32m", // Green
95..=100 => "\x1b[31m", // Red
_ => "\x1b[0m", // Default (shouldn't happen)
0..=45 => "\x1b[31m", // Red
50..=65 => "\x1b[38;5;173m", // Orange
70..=80 => "\x1b[33m", // Yellow
85..=95 => "\x1b[32m", // Green
96..=100 => "\x1b[33m", // Yellow
_ => "\x1b[0m", // Default (shouldn't happen)
};
let reset_code = "\x1b[0m";
@@ -204,8 +205,20 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
})
.collect();
let trailing = match 100 - i * 5 {
95 => "◀━┓",
60 => " ┣━━━━━━━━━━━━┓",
55 => " ┃ 90% ┃",
50 => " ┃ Confidence ┃",
45 => " ┃ Interval ┃",
40 => " ┣━━━━━━━━━━━━┛",
5 => "◀━┛",
6..=94 => " ┃",
_ => "",
};
println!(
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}",
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}│{}{:5.0} days{}│{}{}{}{}",
color_code,
format!("p{}", 100 - i * 5),
reset_code,
@@ -214,8 +227,15 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
reset_code,
color_code,
days,
reset_code
reset_code,
color_code,
calendar_duration.num_days(),
reset_code,
color_code,
end_date,
reset_code,
trailing
);
}
println!("────┴────────────────────────────────────────────────────────────┴──────────");
println!("────┴────────────────────────────────────────────────────────────┴──────────┴──────────┴──────────");
}
+41 -38
View File
@@ -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
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 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
View File
@@ -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);
+7
View File
@@ -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();
+3 -4
View File
@@ -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);
+8 -2
View File
@@ -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");
+4 -4
View File
@@ -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);
+15 -5
View File
@@ -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);