A Python implementation of the Critical Path Method (CPM) and Program Evaluation and Review Technique (PERT) for project planning, scheduling, and probabilistic analysis.
Built for the Operations Research course — zero external dependencies beyond matplotlib (optional, for visualizations) and tabulate (for pretty-printed tables).
- The Algorithm at a Glance
- Project Structure
- Setup
- Quick Start
- CLI Usage
- Library Usage
- Visualizations
- Running Tests
- API Reference
- Dependencies
- License
| Method | Duration Model | What It Tells You |
|---|---|---|
| CPM | Deterministic (single value per activity) | Critical path, float/slack, project duration |
| PERT | Probabilistic (optimistic, most likely, pessimistic) | Expected duration, variance, probability of meeting a deadline |
Activities → Forward Pass → Backward Pass → Float → Critical Path
- Forward Pass: Computes Early Start (ES) and Early Finish (EF) for every activity.
ESⱼ = max(EFᵢ)for all predecessorsiofjEFⱼ = ESⱼ + dⱼ
- Backward Pass: Computes Late Start (LS) and Late Finish (LF).
LFᵢ = min(LSⱼ)for all successorsjofiLSᵢ = LFᵢ - dᵢ
- Total Float (Slack):
TF = LS − ES(orLF − EF) - Critical Path: Activities with
TF = 0— any delay here delays the whole project.
Uses the beta distribution for each activity's duration:
Expected duration: tₑ = (O + 4M + P) / 6
Variance: σ² = ((P − O) / 6)²
Where:
- O = optimistic estimate (everything goes right)
- M = most likely estimate (business as usual)
- P = pessimistic estimate (everything goes wrong)
The project duration follows a normal distribution (Central Limit Theorem), so we can compute:
- Probability of completion by a given date:
P(T ≤ target) - Days needed for a given confidence level (e.g., 95% confidence)
cpm_pert/
├── main.py # CLI entry point
├── pyproject.toml # Project configuration (uv)
├── README.md # You are here
│
├── cpm_pert/ # Library package
│ ├── __init__.py # Public API exports
│ ├── activity.py # Data models: Activity, CPMProject, PERTProject, PertEstimate
│ ├── forward_pass.py # ES / EF calculation
│ ├── backward_pass.py # LS / LF / Total Float calculation
│ ├── critical_path.py # Critical path identification & table report
│ ├── pert.py # PERT probabilistic metrics & analysis
│ └── visualize.py # Network diagram & Gantt chart (matplotlib)
│
├── tests/
│ ├── test_cpm.py # 10 unit tests
│
└── .venv/ # Virtual environment (uv)
- Python ≥ 3.14
- uv (fast Python package manager)
# 1. Clone or navigate to the project directory
cd /home/sergio/Documents/Escuela/Investigacion\ operaciones/cpm_pert
# 2. Create virtual environment and install dependencies
uv sync
# 3. Activate the virtual environment (optional, uv run works without it)
source .venv/bin/activateThat's it. No extra steps needed.
# Run the bundled example (from the parcial)
uv run python main.py
# With PERT probabilistic analysis
uv run python main.py --pert
# With visualizations
uv run python main.py --network --gantt
# Bigger project (12 activities)
uv run python main.py --build
# Everything at once
uv run python main.py --pert --network --gantt+-------+-------+------+------+------+------+-----------+--------+
| Act | Dur | ES | EF | LS | LF | Holgura | Crít |
+=======+=======+======+======+======+======+===========+========+
| A | 2 | 0.0 | 2.0 | 0.0 | 2.0 | 0.0 | ✓ |
| B | 3 | 2.0 | 5.0 | 2.0 | 5.0 | 0.0 | ✓ |
| C | 6 | 2.0 | 8.0 | 4.0 | 10.0 | 2.0 | |
| D | 5 | 5.0 | 10.0 | 5.0 | 10.0 | 0.0 | ✓ |
| E | 3 | 10.0 | 13.0 | 10.0 | 13.0 | 0.0 | ✓ |
| F | 0 | 10.0 | 10.0 | 13.0 | 13.0 | 3.0 | |
| G | 4 | 13.0 | 17.0 | 13.0 | 17.0 | 0.0 | ✓ |
+-------+-------+------+------+------+------+-----------+--------+
📏 Project duration: 17.0 days
🔴 Critical path: A → B → D → E → G
python main.py [OPTIONS]| Option | Description |
|---|---|
--pert |
Include PERT probabilistic analysis |
--build |
Use the larger construction project (12 activities) |
--network |
Show the network dependency graph |
--gantt |
Show the Gantt chart |
--save PATH |
Save charts to disk (appends _network.png / _gantt.png) |
# PERT analysis with confidence levels
uv run python main.py --pert
# Save visualizations to files
uv run python main.py --network --gantt --save ./output/chart
# Full analysis: larger project + PERT + network graph
uv run python main.py --build --pert --networkYou can import and use the library directly in your own scripts.
from cpm_pert.activity import Activity, CPMProject
from cpm_pert.forward_pass import forward_pass
from cpm_pert.backward_pass import backward_pass
from cpm_pert.critical_path import find_critical_path, print_schedule
# 1. Define activities
project = CPMProject()
project.add_activities(
Activity("A", "Foundation", 3),
Activity("B", "Framing", 5, predecessors=["A"]),
Activity("C", "Roofing", 4, predecessors=["B"]),
Activity("D", "Electrical", 3, predecessors=["B"]),
Activity("E", "Finishing", 2, predecessors=["C", "D"]),
)
# 2. Run the CPM pipeline
forward_pass(project)
backward_pass(project)
find_critical_path(project)
# 3. Print results
print_schedule(project)
# 4. Access data programmatically
for act in project.activities.values():
print(f"{act.id}: ES={act.early_start}, EF={act.early_finish}, "
f"LS={act.late_start}, LF={act.late_finish}, "
f"Float={act.total_float}, Critical={act.is_critical}")
critical_ids = [aid for aid, a in project.activities.items() if a.is_critical]
print(f"Critical path: {' → '.join(critical_ids)}")from cpm_pert.activity import Activity, CPMProject, PertEstimate
from cpm_pert.forward_pass import forward_pass
from cpm_pert.backward_pass import backward_pass
from cpm_pert.critical_path import find_critical_path
from cpm_pert.pert import compute_pert_metrics
# 1. Define activities with PERT estimates
project = CPMProject()
project.add_activities(
Activity("A", "Requirements",
pert=PertEstimate(optimistic=1, most_likely=2, pessimistic=4)),
Activity("B", "Development",
pert=PertEstimate(optimistic=4, most_likely=7, pessimistic=12),
predecessors=["A"]),
Activity("C", "Testing",
pert=PertEstimate(optimistic=1, most_likely=2, pessimistic=3),
predecessors=["B"]),
)
# 2. Run CPM pipeline
forward_pass(project)
backward_pass(project)
find_critical_path(project)
# 3. Compute PERT metrics
pert = compute_pert_metrics(project)
# 4. Query probabilities
prob_15 = pert.probability_of_completion(15.0) # P(T ≤ 15 days)
prob_20 = pert.probability_of_completion(20.0) # P(T ≤ 20 days)
# 5. Days needed for a given confidence
days_90 = pert.days_for_probability(0.90) # 90% confidence
days_95 = pert.days_for_probability(0.95) # 95% confidence
print(f"Expected project duration: {pert.critical_path_duration:.2f} days")
print(f"Variance: {pert.critical_path_variance:.4f}")
print(f"P(T ≤ 15 days): {prob_15 * 100:.1f}%")
print(f"P(T ≤ 20 days): {prob_20 * 100:.1f}%")
print(f"90% confidence: {days_90:.2f} days")Shows the activity dependency graph. Red nodes are critical activities, blue nodes are non-critical. Arrow direction indicates predecessor → successor.
uv run python main.py --networkWhat to look for:
- Red nodes = critical path (any delay here delays the whole project)
- Blue nodes = non-critical (have float/slack)
- Numbers below each node show the activity duration
Shows each activity as a horizontal bar. The solid bar is the activity duration; the transparent extension shows available float.
uv run python main.py --ganttWhat to look for:
- Red bars = critical activities (no float)
- Blue bars = non-critical activities
- Transparent extensions = available float for each activity
- Dashed red line = project completion date
uv run python main.py --network --gantt --save ./my_project
# Produces: ./my_project_network.png, ./my_project_gantt.pngNote: On headless servers (no display), charts are saved with the
Aggbackend automatically. Use--saveto persist them.
uv run pytest tests/ -vAll 10 tests cover:
| Test | What it verifies |
|---|---|
test_simple_linear |
Forward/backward pass on A→B→C chain |
test_concurrent |
Parallel activities, correct critical path |
test_pert_estimate |
Beta distribution formulas (tₑ, σ²) |
test_pert_project |
Full PERT pipeline with mixed estimates |
test_pert_probability |
Completion probability calculations |
test_pert_ppf |
Normal quantile function (Acklam approximation) |
test_float_precision |
Zero-float tolerance for critical detection |
test_empty_predecessors |
Activities without predecessors start at 0 |
test_build_schedule |
Table output produces correct number of rows |
test_pert_invalid_estimates |
O ≤ M ≤ P validation |
uv run pytest tests/ -v --tb=shortRepresents a single activity in the project network.
| Field | Type | Default | Description |
|---|---|---|---|
id |
str |
— | Unique identifier (e.g., "A", "Task1") |
description |
str |
"" |
Human-readable description |
duration |
float |
0.0 |
Deterministic duration (CPM) |
predecessors |
list[str] |
[] |
IDs of activities that must precede this one |
pert |
PertEstimate | None |
None |
PERT probabilistic estimate (overrides duration) |
Computed fields (populated after forward/backward pass):
| Property | Type | Description |
|---|---|---|
early_start / early_finish |
float |
Forward pass results |
late_start / late_finish |
float |
Backward pass results |
total_float |
float |
LS − ES |
is_critical |
bool |
True if total_float ≈ 0 |
effective_duration |
float |
pert.expected if PERT is set, else duration |
pert_variance |
float |
PERT variance if applicable |
Container for a set of activities and the CPM analysis results.
| Method | Description |
|---|---|
add_activity(activity) |
Add a single Activity |
add_activities(*activities) |
Add multiple activities at once |
get_activity(id) -> Activity |
Look up an activity by ID |
critical_activities -> list[Activity] |
All critical activities |
| Field | Type | Description |
|---|---|---|
activities |
dict[str, Activity] |
Activity lookup by ID |
project_duration |
float |
Total project duration (max EF, set by forward_pass) |
Probabilistic project analysis, built after CPM computation.
| Method | Description |
|---|---|
probability_of_completion(target_days) -> float |
P(T ≤ target) using normal CDF |
days_for_probability(probability) -> float |
Inverse: days needed for a given confidence level |
| Field | Type | Description |
|---|---|---|
project_cpm |
CPMProject |
The underlying CPM project |
critical_path_duration |
float |
Sum of expected durations on critical path |
critical_path_variance |
float |
Sum of variances on critical path |
Immutable container for PERT's three-point estimate.
| Field | Type | Constraint |
|---|---|---|
optimistic |
float |
≤ most_likely |
most_likely |
float |
≤ pessimistic |
pessimistic |
float |
≥ most_likely |
| Property | Formula | Description |
|---|---|---|
expected |
(O + 4M + P) / 6 |
Expected duration (beta distribution mean) |
variance |
((P − O) / 6)² |
Duration variance |
std_dev |
√variance |
Standard deviation |
| Function | Input | Output | Description |
|---|---|---|---|
forward_pass(project) |
CPMProject |
CPMProject (mutated) |
Computes ES/EF for all activities |
backward_pass(project) |
CPMProject (post-forward) |
CPMProject (mutated) |
Computes LS/LF/total_float |
find_critical_path(project) |
CPMProject (post-backward) |
CPMProject (mutated) |
Marks activities with float ≈ 0 as critical |
print_schedule(project) |
CPMProject |
stdout | Prints the full schedule as a table |
build_schedule(project) |
CPMProject |
list[dict] |
Returns schedule data as a list of dicts |
get_critical_path_ids(project) |
CPMProject |
list[str] |
Returns critical path activity IDs in order |
compute_pert_metrics(project) |
CPMProject |
PERTProject |
Computes probabilistic metrics from critical path |
plot_network(project, ...) |
CPMProject |
plt.Figure |
Draws the dependency graph |
plot_gantt(project, ...) |
CPMProject |
plt.Figure |
Draws the Gantt chart |
| Package | Version | Required? | Purpose |
|---|---|---|---|
tabulate |
≥ 0.10.0 | ✅ Yes | Pretty-printed schedule tables |
matplotlib |
≥ 3.11.0 | ✅ Yes | Network diagram & Gantt charts |
numpy |
≥ 2.x | 🔄 Transitive | Required by matplotlib only |
pytest |
≥ 9.1.1 | ❌ Dev only | Unit testing |
The dependency graph is built from scratch using a topological level sorting algorithm — no need for a full graph library for DAG-based project scheduling.
Educational project — free to use, modify, and share for academic purposes.
Part of the Operations Research coursework — "Investigación de Operaciones".