Skip to main content

pmcontrols

CI PyPI License: MIT

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 Result with 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 predecessors
  • pert: 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 index
  • crash: 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), and mc_distribution (completion histogram)
  • Result: every analysis returns named statistics, a tidy table, structured alerts, r.ok, r.summary(), and a JSON-safe to_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)

Source distribution for pmcontrols 0.2.1
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)

Table of built distributions (wheels) for pmcontrols 0.2.1
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page