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.
Metadata
Release files for pmcontrols 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pmcontrols-0.2.1.tar.gz | 23.1 kB | With provenance | Details for pmcontrols-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pmcontrols-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for pmcontrols-0.2.1-py3-none-any.whl |
Total release size: 42.3 kB
Release files / pmcontrols-0.2.1.tar.gz
| Download URL | pmcontrols-0.2.1.tar.gz |
|---|---|
| Size | 23.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
835630376b9507a54216df47fcdb105c9aaa8cc3046d287275c61dcdbfa626f8
|
|
BLAKE2b-256 checksum How to use checksums |
8d73106a62fb00ba1d8ec340693adb670a0acfdc680477eab487178c747f419b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 15, 2026.
Transparency logRelease files / pmcontrols-0.2.1-py3-none-any.whl
| Download URL | pmcontrols-0.2.1-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3596f9f26dc992248264e217fa22dfb1086f95553e1bc565cde45a79f909e96d
|
|
BLAKE2b-256 checksum How to use checksums |
4589224d9f89fd05a477a36d91b5b0412e3395b0c53ecf663a55bb8a794f3675
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 15, 2026.
Transparency logSecurity for pmcontrols 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pmcontrols-0.2.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 15, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.