Project scheduling and earned value control for Python: CPM, PERT with Monte Carlo schedule risk, minimum-cost crashing, EVM and earned schedule, validated against published reference values.
Project description
pmcontrols
Project scheduling and earned value control for Python.
Critical path and PERT scheduling, minimum-cost schedule compression, and earned value management with earned schedule. Results are computed from the standard formulations and checked against reference values in the test suite.
Motivation
Every project office computes CPI and SPI, and almost all of it happens in spreadsheets. R and commercial tools (Primavera, @RISK) cover schedule risk and earned value. In Python the picture is uneven: the scheduling basics exist, but the cost and forecasting side is thin, and nothing ties them together under one validated interface.
| Method | State of the Python ecosystem |
|---|---|
| Critical path and PERT | available (pyCritical, networkx, assorted scripts) |
| Schedule crashing (time/cost LP) | no packaged library; built ad hoc on PuLP or SciPy |
| Earned value, full indicator set | only minimal or scattered implementations |
| Earned schedule (Lipke SPI(t)) | no maintained package |
| All of the above, validated, under one API | none |
pmcontrols is an attempt to fill that gap, with a few specific goals:
- compute every result from the defining formulation (the forward and backward pass, the linear program, Lipke's interpolation), not from spreadsheet shortcuts
- return one consistent
Resultwith provenance, so a number can be recomputed and audited months later - validate every released number against published reference values on each CI run
- work headless, so a weekly cost and schedule review can run as a cron job
pip install pmcontrols
For Gantt charts, add the optional plotting extra:
pip install "pmcontrols[plot]"
Documentation: https://arikanatakan.github.io/pmcontrols/
Status
Version 0.2.1 is on PyPI. Implemented and tested:
cpm: forward and backward pass returning ES, EF, LS, LF, total slack, and the zero-float critical path, with clear errors on cycles and unknown predecessorspert: three-point estimates (te = (a + 4m + b) / 6) along the critical path, plus a Monte Carlo simulation reporting the completion distribution (p50, p80, p95) and a per-activity criticality indexcrash: minimum-cost schedule compression to a target date, solved as the classical time/cost trade-off linear program (scipy, HiGHS)evm,plan,earned_schedule: cost and schedule variance, CPI and SPI, the estimate-at-completion family, TCPI and VAC, and Lipke's earned schedule (ES, SPI(t), IEAC(t)) evaluated against a frozen baseline- visualization (optional, matplotlib via the
[plot]extra), kept separate from the statistics:gantt(schedule),network_diagram(activity network with the critical path),evm_curve(earned value S-curve),criticality(Monte Carlo criticality bars), andmc_distribution(completion histogram) Result: every analysis returns named statistics, a tidy table, structured alerts,r.ok,r.summary(), and a JSON-safeto_dict()with provenance (version, input hash, timestamp)PMB: a Performance Measurement Baseline that validates a non-decreasing planned-value curve and round-trips through JSON- a reference-case validation suite (tests/validation_cases.json), with the General Foundry network (full schedule, critical path, and optimal crash costs) and hand-derived EVM and earned-schedule cases reproduced in CI across Python 3.10 to 3.12
Usage
Critical path, slack, and the full schedule:
import pmcontrols as pm
activities = [
{"id": "A", "predecessors": [], "duration": 2},
{"id": "B", "predecessors": [], "duration": 3},
{"id": "C", "predecessors": ["A"], "duration": 2},
{"id": "D", "predecessors": ["B"], "duration": 4},
{"id": "E", "predecessors": ["C"], "duration": 4},
{"id": "F", "predecessors": ["C"], "duration": 3},
{"id": "G", "predecessors": ["D", "E"], "duration": 5},
{"id": "H", "predecessors": ["F", "G"], "duration": 2},
]
r = pm.cpm(activities)
r.stats["project_duration"] # 15.0
r.meta["critical_activities"] # ['A', 'C', 'E', 'G', 'H']
r.table # ES/EF/LS/LF/slack per activity
The numbers above are the standard General Foundry network from Render, Stair & Hanna; the 15-period schedule and the A-C-E-G-H critical path are computed by the forward and backward pass, and reproduced exactly as a validation case.
Draw that schedule as a Gantt chart (needs pip install "pmcontrols[plot]"):
fig, ax = pm.gantt(r) # critical path in red, total float in light grey
fig.savefig("schedule.png")
The same extra gives pm.network_diagram(r), pm.evm_curve(pmb, evm_result),
pm.criticality(pert_result), and pm.mc_distribution(pert_result).
Schedule risk from three-point estimates, with the Monte Carlo completion distribution the analytic method cannot give:
r = pm.pert([
{"id": "X", "predecessors": [], "a": 1, "m": 2, "b": 9},
{"id": "Y", "predecessors": ["X"], "a": 2, "m": 3, "b": 10},
])
r.stats["mc_p80"] # 80th-percentile completion time
r.table["criticality_index"] # per activity, from the simulation
The cheapest way to hit a deadline, as a linear program rather than by hand:
r = pm.crash(crash_activities, target=13)
r.stats["total_crash_cost"] # globally optimal, not greedy
r.table["crash_amount"] # how much to compress each activity
Freeze the planned value curve once, then control against it every period:
import sys
pm.plan(periods, pv).save("pmb.json") # commit this to version control
r = pm.evm("pmb.json", ev=30_000, ac=35_000, at=4)
r.ok # False if CPI or SPI(t) is below threshold
r.summary() # plain text verdict
r.stats["ieac_t"] # earned-schedule duration forecast
sys.exit(0 if r.ok else 1)
Every analysis returns the same Result object: named statistics, a tidy
table, a tuple of structured alerts, and provenance metadata (library
version, input hash, timestamp).
If you are wiring this into an AI agent, read Project control is not a language task first.
Roadmap
| Version | Scope |
|---|---|
| 0.3 | published PMI/Lipke earned-schedule case-study validation; OC plotting; resource leveling and constrained scheduling; EVM from time-phased ledgers |
| 0.4 | critical chain buffers; probabilistic crashing; earned-schedule forecasting variants; JOSS paper |
Out of scope: Gantt-chart project editors (use dedicated PM tools), process control, acceptance sampling (see lotsampling), general linear programming (use scipy or pyomo directly).
License
MIT. Written and maintained by Atakan Arikan, MSc Student at Tsinghua University and Politecnico di Milano.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pmcontrols-0.2.1.tar.gz.
File metadata
- Download URL: pmcontrols-0.2.1.tar.gz
- Upload date:
- Size: 23.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
835630376b9507a54216df47fcdb105c9aaa8cc3046d287275c61dcdbfa626f8
|
|
| MD5 |
2ee26b43e9c6f331c503ce51244faf7e
|
|
| BLAKE2b-256 |
8d73106a62fb00ba1d8ec340693adb670a0acfdc680477eab487178c747f419b
|
Provenance
The following attestation bundles were made for pmcontrols-0.2.1.tar.gz:
Publisher:
release.yml on arikanatakan/pmcontrols
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pmcontrols-0.2.1.tar.gz -
Subject digest:
835630376b9507a54216df47fcdb105c9aaa8cc3046d287275c61dcdbfa626f8 - Sigstore transparency entry: 1820901549
- Sigstore integration time:
-
Permalink:
arikanatakan/pmcontrols@f0a716b8a2ba1ab669c3ce9c0a3971b5154d2bf8 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/arikanatakan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f0a716b8a2ba1ab669c3ce9c0a3971b5154d2bf8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pmcontrols-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pmcontrols-0.2.1-py3-none-any.whl
- Upload date:
- Size: 19.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3596f9f26dc992248264e217fa22dfb1086f95553e1bc565cde45a79f909e96d
|
|
| MD5 |
c7d9841e08a18d4a886bb29b5c6cf028
|
|
| BLAKE2b-256 |
4589224d9f89fd05a477a36d91b5b0412e3395b0c53ecf663a55bb8a794f3675
|
Provenance
The following attestation bundles were made for pmcontrols-0.2.1-py3-none-any.whl:
Publisher:
release.yml on arikanatakan/pmcontrols
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pmcontrols-0.2.1-py3-none-any.whl -
Subject digest:
3596f9f26dc992248264e217fa22dfb1086f95553e1bc565cde45a79f909e96d - Sigstore transparency entry: 1820901620
- Sigstore integration time:
-
Permalink:
arikanatakan/pmcontrols@f0a716b8a2ba1ab669c3ce9c0a3971b5154d2bf8 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/arikanatakan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f0a716b8a2ba1ab669c3ce9c0a3971b5154d2bf8 -
Trigger Event:
push
-
Statement type: