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.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.15"
@@ -51,6 +66,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "autocfg"
version = "1.3.0"
@@ -63,12 +87,33 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -76,10 +121,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.15"
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "clap"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
]
@@ -108,6 +168,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
@@ -133,6 +199,17 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "distimate"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e266cc5215f2a854deadfe97b5fb1047ab35121fd4fafa958e3760945790bf"
dependencies = [
"rand",
"statrs",
"thiserror",
]
[[package]]
name = "either"
version = "1.13.0"
@@ -178,6 +255,29 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.4.0"
@@ -201,10 +301,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "libc"
version = "0.2.155"
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libm"
@@ -219,10 +328,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "mcps"
version = "0.1.1"
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "matrixmultiply"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "mcps"
version = "0.2.0"
dependencies = [
"chrono",
"clap",
"distimate",
"rand",
"rand_distr",
"rayon",
@@ -230,6 +357,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"tempfile",
"workdays",
]
[[package]]
@@ -238,6 +366,63 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nalgebra"
version = "0.32.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4"
dependencies = [
"approx",
"matrixmultiply",
"nalgebra-macros",
"num-complex",
"num-rational",
"num-traits",
"rand",
"rand_distr",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-macros"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -254,6 +439,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@@ -321,6 +512,12 @@ dependencies = [
"rand",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.10.0"
@@ -361,19 +558,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.207"
name = "safe_arch"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a"
dependencies = [
"bytemuck",
]
[[package]]
name = "serde"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.207"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
@@ -382,9 +588,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.124"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
"itoa",
"memchr",
@@ -405,6 +611,37 @@ dependencies = [
"unsafe-libyaml",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simba"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]]
name = "statrs"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f697a07e4606a0a25c044de247e583a330dbb1731d11bc7350b81f48ad567255"
dependencies = [
"approx",
"nalgebra",
"num-traits",
"rand",
]
[[package]]
name = "strsim"
version = "0.11.1"
@@ -413,9 +650,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.74"
version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [
"proc-macro2",
"quote",
@@ -435,6 +672,32 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.12"
@@ -459,6 +722,80 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "wide"
version = "0.7.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -541,6 +878,18 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "workdays"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e3bc4b29b64419a2983662ca6902adf5313df2a061e8f6104b2dd603765b5aa"
dependencies = [
"chrono",
"serde",
"serde_json",
"serde_yaml",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
+4 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mcps"
version = "0.1.1"
version = "0.2.0"
authors = ["Stephen Waits <steve@waits.net>"]
edition = "2021"
description = "A command-line tool for running Monte Carlo simulations on project schedules."
@@ -22,13 +22,16 @@ name = "mcps"
path = "src/main.rs"
[dependencies]
chrono = "0.4.38"
clap = "4.5.15"
distimate = "0.2.0"
rand = "0.8.5"
rand_distr = "0.4.3"
rayon = "1.10.0"
serde = { version = "1.0.207", features = ["derive"] }
serde_json = "1.0.124"
serde_yaml = "0.9.34"
workdays = "0.1.0"
[dev-dependencies]
tempfile = "3.12.0"
+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
required. It helps project managers and engineers gain a better understanding of
potential project risks and timelines by providing a probabilistic analysis of
schedule durations and total work effort.
project durations and total work effort.
## Features
- **Simulate Project Schedules**: Run multiple simulations to estimate the
probability distribution of project completion times and total work effort.
- **Customizable Inputs**: Accepts project schedules in YAML or JSON formats,
- **Customizable Inputs**: Accepts project definitions in YAML or JSON formats,
with options to override certain parameters like the number of workers or the
number of iterations.
- **Visual Output**: Generates an ASCII-based cumulative distribution function
@@ -34,6 +34,9 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install mcps
```
Note, if you're upgrading, use `cargo install --force mcps` to force
installation of the latest version..
### Option 2: Clone and build the repository locally
```bash
@@ -46,45 +49,79 @@ The binary will be available at `target/release/mcps`.
## Usage
Once you have built the tool, you can use it by providing a project schedule
Once you have built the tool, you can use it by providing a project definition
file in YAML or JSON format.
### Basic Usage
```bash
mcps example.yaml -i 100000 -w 10
mcps project.yaml -i 100000 -w 10
```
This command runs the Monte Carlo simulation on the [`example.yaml`](./example.yaml) schedule file
with 100,000 iterations and overrides the number of workers to 10.
This command runs the Monte Carlo simulation on the
[`project.yaml`](./assets/project.yaml) project file with 100,000 iterations
and overrides the number of workers to 10.
### Command-Line Options
- `-i, --iterations <iterations>`: Specify the number of iterations to run. Must be at least 100. Default is 50,000.
- `-w, --workers <num_workers>`: Override the number of workers specified in the schedule file.
- `-v, --version`: Print version information.
- `-h, --help`: Print help information.
- `-i, --iterations <iterations>`: Specify the number of iterations to run. Must
be at least 100. Default is 50,000.
- `-n, --workers <num_workers>`: Override `num_workers` specified in project file
- `-b, --begin <YYYY-MM-DD>`: Override `start_date` specified in project file
- `-s, --schedule <filename>`: Work schedule config file (.yaml or .json)
- `-h, --help`: Print help
- `-V, --version`: Print version
### Schedule File Format
### Project Definition File Format
The tool accepts schedule files in YAML or JSON format. Below is an example of the YAML format:
`mcps` accepts projects in YAML or JSON format. Below is an example of the
YAML format:
```yaml
num_workers: 5 # for the purpose of scheduling simulation
estimate_confidence: 0.80 # confidence actuals will fall inside estimate bounds
start_date: 2024-08-01 # [optional] project start date
tasks:
- id: DesignPhase
min_time: 1.5 # 1.5 days
max_time: 3.5 # 3.5 days
estimate:
min: 1.5 # 1.5 days
likely: 2.2 # 2.2 days (best guess at actual time)
max: 3.5 # 3.5 days
dependencies: []
- id: ImplementationPhase
min_time: 2.25 # 2.25 days
max_time: 4.75 # 4.75 days
estimate:
min: 2.25 # 2.25 days
likely: 3.9 # 3.9 days (best guess at actual time)
max: 4.75 # 4.75 days
dependencies: [DesignPhase]
```
This repo includes examples in [JSON](./example.json) and
[YAML](./example.yaml).
This repo includes examples in [JSON](./assets/project.json) and
[YAML](./assets/project.yaml).
### Work Schedule Configuration File Format
This file is optional. It allows you to configure the days of the week you work
along with any specific work holidays.
If you don't provide a schedule file, the tool will default to a 5-day work week
with no scheduled holidays.
`mcps` accepts schedules in YAML or JSON format. Below is an example of the
YAML format:
work_days:
```yaml
work_days:
- Monday
- Tuesday
- Wednesday
holidays:
- 2023-12-25
```
This repo includes examples in [JSON](./assets/schedule.json) and
[YAML](./assets/schedule.yaml).
### Example Output
@@ -111,4 +148,5 @@ always welcome!
### License
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
This project is licensed under the MIT License. See the [LICENSE](./LICENSE)
file for details.
+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,
"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
+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 workdays::WorkCalendar;
use std::time::Duration;
use std::{str::FromStr, time::Duration};
const AFTER_HELP_TEXT: &str = "\
Examples of supported file formats:
Find example project definition and schedule config files in the repository:
YAML Format:
https://github.com/swaits/mcps/blob/main/examples/
num_workers: 5
estimate_confidence: 0.80
tasks:
- id: DesignPhase
min_time: 1.5 # 1.5 days
max_time: 3.5 # 3.5 days
dependencies: []
- id: ImplementationPhase
min_time: 2.25 # 2.25 days
max_time: 4.75 # 4.75 days
dependencies: [DesignPhase]
JSON Format:
{
\"num_workers\": 5,
\"estimate_confidence\": 0.80,
\"tasks\": [
{
\"id\": \"DesignPhase\",
\"min_time\": 1.5,
\"max_time\": 3.5,
\"dependencies\": []
},
{
\"id\": \"ImplementationPhase\",
\"min_time\": 2.25,
\"max_time\": 4.75,
\"dependencies\": [\"DesignPhase\"]
}
]
}
Note: `estimate_confidence` is the confidence (0.0,1.0) that the
actual values fall in the task estimate ranges. For example,
0.8 means you believe that the actual time it will take to
complete a task falls in your [min_time,max_time] estimate
range for that task 80% of the time.";
";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let matches = Command::new("Monte Carlo Project Scheduler")
@@ -55,7 +20,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.about("Runs Monte Carlo simulations on project schedules")
.arg(
Arg::new("filename")
.help("Path to the schedule file (.yaml or .json).")
.help("Path to the project file (.yaml or .json)")
.required(true)
.index(1),
)
@@ -68,15 +33,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
)
.arg(
Arg::new("workers")
.short('w')
.short('n')
.long("workers")
.help("Override `num_workers` specified in schedule file")
.help("Override `num_workers` specified in project file")
.value_name("num_workers"),
)
.arg(
Arg::new("begin")
.short('b')
.long("begin")
.help("Override `start_date` specified in project file")
.value_name("YYYY-MM-DD"),
)
.arg(
Arg::new("schedule")
.short('s')
.long("schedule")
.help("Work schedule config file (.yaml or .json)")
.value_name("filename"),
)
.after_help(AFTER_HELP_TEXT)
.get_matches();
let schedule_path = matches.get_one::<String>("filename").unwrap();
let project_path = matches.get_one::<String>("filename").unwrap();
let num_simulations: usize = matches.get_one::<String>("iterations").unwrap().parse()?;
// Ensure iterations is >= 100
@@ -84,8 +63,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
return Err("Iterations must be at least 100.".into());
}
// Load the schedule
let mut schedule = Schedule::from_file(schedule_path)?;
// Load the project
let mut project = Project::from_file(project_path)?;
// Load work schedule if it exists
let calendar: WorkCalendar = match matches.get_one::<String>("schedule") {
Some(filename) => WorkCalendar::from_str(&std::fs::read_to_string(filename)?)?,
None => WorkCalendar::new(),
};
// Determine the start date (command line > project file > TODAY)
let start_date = matches.get_one::<String>("begin")
.and_then(|date| NaiveDate::parse_from_str(date, "%Y-%m-%d").ok())
.or(project.start_date)
.unwrap_or_else(|| Utc::now().date_naive());
// Check if workers are overridden by command-line argument
if let Some(workers_str) = matches.get_one::<String>("workers") {
@@ -93,34 +84,39 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if workers < 1 {
return Err("Invalid number of workers, must be 1 or more".into());
}
schedule.num_workers = workers;
project.num_workers = workers;
}
// Monte Carlo simulation
let (project_durations, effort_times) = run_multiple_simulations(&schedule, num_simulations);
let (project_durations, effort_times) = run_multiple_simulations(&project, num_simulations);
// Results output
print_ascii_cdf(
&project_durations,
format!(
"Project Completion Time with {} Worker{} (by probability)",
schedule.num_workers,
if schedule.num_workers == 1 { "" } else { "s" }
"Completion Time ({} Worker{}, starting {})",
project.num_workers,
if project.num_workers == 1 { "" } else { "s" },
start_date,
)
.as_str(),
"Duration",
&start_date,
&calendar,
);
println!();
print_ascii_cdf(
&effort_times,
"Total Work Effort (by probability)",
"Effort",
format!("Total Work Effort (1 worker, starting {})", start_date).as_str(),
&start_date,
&calendar,
);
Ok(())
}
fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
fn print_ascii_cdf(data: &[Duration], title: &str, start: &NaiveDate, calendar: &WorkCalendar) {
let mut sorted_data = data.to_vec();
sorted_data.sort_unstable();
let min = sorted_data[0];
@@ -139,9 +135,9 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
padding_right = width - padding - title.len()
);
println!("────┬────────────────────────────────────────────────────────────┬──────────");
println!("%ile│{}│{:>10}", centered_title, units);
println!("────┼────────────────────────────────────────────────────────────┼──────────");
println!("────┬────────────────────────────────────────────────────────────┬──────────┬──────────┬──────────");
println!("%ile│{}│ Workdays │ Schedule │ Complete ", centered_title);
println!("────┼────────────────────────────────────────────────────────────┼──────────┼──────────┼──────────");
let bar_width = 60;
@@ -179,6 +175,9 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
let offset = (bar_width - (max_bar_position - min_bar_position)) / 2;
for (i, (bar_position, days)) in bar_positions.iter().enumerate() {
let (end_date, calendar_duration) =
calendar.compute_end_date(*start, *days as i64).unwrap();
let shifted_bar_position = bar_position + offset;
let (fg, bg) = if i % 2 == 0 {
@@ -191,7 +190,7 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
0..=50 => "\x1b[31m", // Red
55..=75 => "\x1b[33m", // Orange
80..=90 => "\x1b[32m", // Green
95..=100 => "\x1b[31m", // Red
95..=100 => "\x1b[33m", // Orange
_ => "\x1b[0m", // Default (shouldn't happen)
};
let reset_code = "\x1b[0m";
@@ -205,7 +204,7 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
.collect();
println!(
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}",
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}│{}{:5.0} days{}│{}{}{}",
color_code,
format!("p{}", 100 - i * 5),
reset_code,
@@ -214,8 +213,14 @@ fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
reset_code,
color_code,
days,
reset_code,
color_code,
calendar_duration.num_days(),
reset_code,
color_code,
end_date,
reset_code
);
}
println!("────┴────────────────────────────────────────────────────────────┴──────────");
println!("────┴────────────────────────────────────────────────────────────┴──────────┴──────────┴──────────");
}
+41 -38
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);