Multi-Phase (Two-Pass) Simulation¶
Multi-phase simulation is an advanced scheduling strategy for resource-constrained projects. When several tasks compete for the same limited pool of resources, the order in which they are dispatched to those resources can materially affect the projected completion date.
The standard scheduler uses a simple greedy policy: when multiple tasks become ready at the same moment it picks them in deterministic ID order. This works well when resources are abundant or contention is low. When resources are scarce and some tasks are strongly critical, a smarter ordering—one that dispatches the most critical tasks first—can reduce the median project duration and, more importantly, reduce tail-risk at the P80– P95 percentiles.
Two-pass simulation solves this problem with two consecutive simulation runs.
Only active with resource-constrained scheduling
Two-pass mode requires at least one named resource in your project file.
If no resources are defined the engine falls back to the standard single-pass mode
automatically.
How It Works¶
Pass 1 (baseline) Pass 2 (priority-ordered)
─────────────────── ─────────────────────────
Greedy dispatch Criticality-ranked dispatch
N = pass1_iterations N = iterations (full run)
┌──────────┐ ┌──────────────────────────┐
│ Sample │ │ Replay pass-1 durations │ ← paired replay
│ durations│ │ (first pass1_iterations) │
└────┬─────┘ │ Sample fresh for the │
│ │ remaining iterations │
▼ └───────────┬──────────────┘
┌──────────────┐ │
│ Build │ ▼
│ criticality │ ┌─────────────────────────┐
│ index CI(t) │ │ Sort ready tasks by │
│ per task │─────────▶│ descending CI → earlier │
└──────────────┘ │ dispatch of critical │
│ tasks │
└──────────┬──────────────┘
│
▼
┌─────────────────────────┐
│ TwoPassDelta: │
│ pass-1 stats │
│ pass-2 stats │
│ Δ = pass-2 − pass-1 │
│ CI per task │
└─────────────────────────┘
Pass 1 — criticality baseline
The engine runs pass1_iterations iterations using the standard greedy policy.
Every iteration, it records which tasks appear on the critical path. The
criticality index of each task is the fraction of pass-1 iterations in which
it was critical (range 0 → 1). Task durations are also cached for paired replay.
Pass 2 — priority-ordered run
The engine runs the full --iterations run again. Whenever two or more tasks
become ready at the same moment and there is a resource to claim, they are sorted
by descending criticality index before dispatch. This ensures that pass-1
identified bottleneck tasks get resources first.
For the first pass1_iterations iterations, the exact same sampled durations
from pass-1 are replayed (paired replay). This makes the comparison between
pass-1 and pass-2 apples-to-apples: any difference in project duration is
caused solely by the change in dispatch policy, not by different duration samples.
TwoPassDelta traceability
The results include a two_pass_traceability block in all output formats
(console, JSON, CSV, HTML) with per-pass statistics and Δ values.
Basic Example¶
Project file¶
For this example we will use the provided examples/contention.yaml file with three tasks sharing one senior developer
and a separate junior developer:
project:
name: "contention-demo"
start_date: "2025-03-01"
hours_per_day: 8
confidence_levels: [50, 80, 90, 95]
tasks:
- id: "A1"
name: "Spec review"
estimate: { low: 9, expected: 10, high: 12 }
resources: ["dev-senior"]
- id: "A2"
name: "Core implementation"
estimate: { low: 13, expected: 15, high: 18 }
dependencies: ["A1"]
resources: ["dev-senior"]
- id: "A3"
name: "Code review"
estimate: { low: 9, expected: 10, high: 12 }
dependencies: ["A2"]
resources: ["dev-senior"]
- id: "B1"
name: "Documentation"
estimate: { low: 18, expected: 20, high: 24 }
resources: ["dev-senior"] # competes for dev-senior!
- id: "B2"
name: "User testing"
estimate: { low: 9, expected: 10, high: 12 }
dependencies: ["B1"]
resources: ["dev-junior"]
- id: "C1"
name: "Integration"
estimate: { low: 4, expected: 5, high: 7 }
dependencies: ["A3", "B2"]
resources: ["dev-junior"]
- id: "C2"
name: "Release"
estimate: { low: 7, expected: 8, high: 10 }
dependencies: ["C1"]
resources: ["dev-junior"]
resources:
- name: "dev-senior"
experience_level: 3
- name: "dev-junior"
experience_level: 1
Single-pass run¶
Output excerpt:
=== Simulation Results ===
Calendar Time Statistical Summary:
┌──────────────────────────┬────────────────────────────────┐
│ Metric │ Value │
├──────────────────────────┼────────────────────────────────┤
│ Mean │ 567.49 hours (71 working days) │
│ Median (P50) │ 559.94 hours │
│ Std Dev │ 13.64 hours │
└──────────────────────────┴────────────────────────────────┘
Calendar Time Confidence Intervals:
┌──────────────┬─────────┬────────────────┬────────────┐
│ Percentile │ Hours │ Working Days │ Date │
├──────────────┼─────────┼────────────────┼────────────┤
│ P50 │ 559.94 │ 70 │ 2025-06-06 │
│ P80 │ 578.91 │ 73 │ 2025-06-11 │
│ P90 │ 580.6 │ 73 │ 2025-06-11 │
│ P95 │ 581.68 │ 73 │ 2025-06-11 │
└──────────────┴─────────┴────────────────┴────────────┘
Two-pass run¶
Add the --two-pass flag:
mcprojsim % mcprojsim simulate examples/contention.yaml -n 1000 --two-pass --pass1-iterations 500 -t --seed 1234
Output excerpt:
Pass 1: computing criticality indices
Progress: 100.0% (500/500)
Pass 2: priority-ordered scheduling
Progress: 100.0% (1000/1000)
=== Simulation Results ===
Calendar Time Statistical Summary:
┌──────────────────────────┬────────────────────────────────┐
│ Metric │ Value │
├──────────────────────────┼────────────────────────────────┤
│ Mean │ 470.91 hours (59 working days) │
│ Median (P50) │ 463.49 hours │
│ Std Dev │ 13.28 hours │
└──────────────────────────┴────────────────────────────────┘
Calendar Time Confidence Intervals:
┌──────────────┬─────────┬────────────────┬────────────┐
│ Percentile │ Hours │ Working Days │ Date │
├──────────────┼─────────┼────────────────┼────────────┤
│ P50 │ 463.49 │ 58 │ 2025-05-21 │
│ P80 │ 482.47 │ 61 │ 2025-05-26 │
│ P90 │ 484.08 │ 61 │ 2025-05-26 │
│ P95 │ 485.34 │ 61 │ 2025-05-26 │
└──────────────┴─────────┴────────────────┴────────────┘
Two-Pass Scheduling Traceability:
┌──────────┬────────────────────┬─────────────────────┬─────────┐
│ Metric │ Pass-1 iter: 500 │ Pass-2 iter: 1000 │ Delta │
├──────────┼────────────────────┼─────────────────────┼─────────┤
│ Mean │ 567.4h │ 470.9h │ -96.5h │
│ P80 │ 578.9h │ 482.5h │ -96.4h │
│ P95 │ 582.0h │ 485.3h │ -96.6h │
└──────────┴────────────────────┴─────────────────────┴─────────┘
Resource wait delta: -121.1h
The two-pass run reduces the mean project duration from ~567 h to ~471 h (~12 working days) purely by prioritising the tasks that pass-1 identified as critical bottlenecks.
When Is Two-Pass Useful?¶
Two-pass simulation is most valuable when all of the following are true:
| Condition | Why it matters |
|---|---|
| Named resources are defined in the project file | Without resources, dispatch order has no effect |
| At least two tasks compete for the same resource | No contention → dispatch order is irrelevant |
| Some tasks have much higher criticality than others | Uniform criticality → reordering has little effect |
| The project has more than ~5–10 tasks | Very small projects often have no slack to reorder |
Typical use-cases
- Software delivery teams where one or two senior engineers are on the critical path and also reviewed by junior work items
- Infrastructure projects where a specialist (network architect, DBA) is a shared bottleneck across multiple workstreams
- Hardware/software co-design where a scarce test bench is contended between parallel integration streams
- Any project where you suspect the default greedy schedule is sub-optimal
When two-pass has little effect
If every task has its own dedicated resource (no contention), the delta will be close
to zero. Example with abundant-resources.yaml (each task on its own resource):
Two-Pass Scheduling Traceability:
Pass-1 iterations: 300 | Pass-2 iterations: 500
Mean: 189.6h (pass-1) → 189.2h (pass-2) [delta -0.3h]
P80: 194.7h (pass-1) → 194.6h (pass-2) [delta -0.1h]
P95: 197.1h (pass-1) → 196.8h (pass-2) [delta -0.3h]
Resource wait delta: +0.0h
The near-zero delta confirms that two-pass adds no distortion when resources are not a bottleneck.
Configuration Reference¶
CLI flags¶
| Flag | Default | Description |
|---|---|---|
--two-pass |
off | Enable two-pass mode for this run |
--pass1-iterations N |
1 000 | Pass-1 iteration budget for criticality ranking |
Both flags are accepted by the simulate command. --pass1-iterations requires
--two-pass to have any effect.
# Basic two-pass run
mcprojsim simulate project.yaml --two-pass
# Fine-tuned: pass-1 with 2 000 iterations, pass-2 with 10 000
mcprojsim simulate project.yaml --two-pass --pass1-iterations 2000 -n 10000
# Export all formats
mcprojsim simulate project.yaml --two-pass -n 5000 -f json,csv,html -o report
Configuration file¶
You can set the assignment mode globally in your ~/.mcprojsim/configuration.yaml or
any config file passed with --config:
constrained_scheduling:
assignment_mode: criticality_two_pass # default: greedy_single_pass
pass1_iterations: 1000 # default: 1000
With this config, every simulate invocation uses two-pass mode automatically for
projects that have resources, with no need for the --two-pass flag.
\newpage
More Complex Example: Developer Allocation¶
Consider a project with three workstreams all sharing a DevOps engineer for deployment tasks:
project:
name: "multi-stream-release"
start_date: "2025-06-01"
hours_per_day: 8
confidence_levels: [50, 80, 90, 95]
tasks:
# --- Stream A: Backend ---
- id: "backend-dev"
name: "Backend development"
estimate: { low: 60, expected: 80, high: 120 }
resources: ["backend-dev-1"]
- id: "backend-deploy"
name: "Backend deployment"
estimate: { low: 8, expected: 12, high: 16 }
dependencies: ["backend-dev"]
resources: ["devops"] # shared bottleneck
# --- Stream B: Frontend ---
- id: "frontend-dev"
name: "Frontend development"
estimate: { low: 40, expected: 60, high: 90 }
resources: ["frontend-dev-1"]
- id: "frontend-deploy"
name: "Frontend deployment"
estimate: { low: 6, expected: 8, high: 12 }
dependencies: ["frontend-dev"]
resources: ["devops"] # shared bottleneck
# --- Stream C: Mobile ---
- id: "mobile-dev"
name: "Mobile development"
estimate: { low: 80, expected: 100, high: 140 }
resources: ["mobile-dev-1"]
- id: "mobile-deploy"
name: "Mobile deployment"
estimate: { low: 8, expected: 10, high: 14 }
dependencies: ["mobile-dev"]
resources: ["devops"] # shared bottleneck
# --- Integration ---
- id: "smoke-test"
name: "Smoke test all streams"
estimate: { low: 8, expected: 12, high: 20 }
dependencies: ["backend-deploy", "frontend-deploy", "mobile-deploy"]
resources: ["qa-1"]
- id: "release"
name: "Production release"
estimate: { low: 4, expected: 6, high: 10 }
dependencies: ["smoke-test"]
resources: ["devops"]
resources:
- name: "backend-dev-1"
experience_level: 3
- name: "frontend-dev-1"
experience_level: 2
- name: "mobile-dev-1"
experience_level: 2
- name: "devops"
experience_level: 3
- name: "qa-1"
experience_level: 2
Run with two-pass to discover which deployment stream should be prioritised:
mcprojsim simulate multi-stream.yaml --two-pass --pass1-iterations 2000 \
-n 10000 -t -f json,html -o multi-stream-report
The TwoPassDelta block in the JSON output will show which stream's deploy task had the highest criticality index in pass-1, and how much the end-to-end schedule improved once pass-2 gave that stream's DevOps slot priority.
\newpage
Output Formats¶
Console (-t table mode)¶
A Two-Pass Scheduling Traceability table is printed after the constrained
diagnostics when --two-pass is active. The table shows pass-1, pass-2, and
Δ values for Mean, P80, and P95. The resource wait delta is printed on a
separate line below the table.
JSON export (-f json)¶
The two_pass_traceability key is always present. It is null for single-pass
runs and an object for two-pass runs:
{
"two_pass_traceability": {
"enabled": true,
"pass1_iterations": 500,
"pass2_iterations": 1000,
"ranking_method": "criticality_index",
"pass1": {
"mean_hours": 519.9,
"p50_hours": 519.8,
"p80_hours": 530.6,
"p90_hours": 532.7,
"p95_hours": 533.0,
"resource_wait_hours": 212.2,
"resource_utilization": 0.87,
"calendar_delay_hours": 0.0
},
"pass2": {
"mean_hours": 422.8,
"p50_hours": 422.3,
"p80_hours": 434.3,
"p90_hours": 436.4,
"p95_hours": 437.1,
"resource_wait_hours": 90.2,
"resource_utilization": 0.91,
"calendar_delay_hours": 0.0
},
"delta": {
"mean_hours": -97.0,
"p50_hours": -97.5,
"p80_hours": -96.3,
"p90_hours": -96.3,
"p95_hours": -95.9,
"resource_wait_hours": -122.0,
"resource_utilization": 0.04,
"calendar_delay_hours": 0.0
},
"task_criticality_index": {
"A1": 0.0,
"A2": 0.0,
"A3": 0.0,
"B1": 1.0,
"B2": 1.0,
"C1": 1.0,
"C2": 1.0
}
}
}
CSV export (-f csv)¶
A Two-Pass Scheduling Traceability section is appended after the constrained diagnostics block, followed by a Task Criticality Index (pass-1) section that lists each task's CI value.
HTML export (-f html)¶
The HTML report gains a Two-Pass Scheduling Traceability table with pass-1, pass-2, and Δ columns for each key metric, followed by a Task Criticality Index (Pass 1) table.
Choosing pass1_iterations¶
Pass-1 is used only to rank tasks; it does not directly affect the final results. A few practical guidelines:
| Scenario | Recommended pass1_iterations |
|---|---|
| Quick exploratory run | 200–500 |
| Standard analysis | 500–2 000 (default 1 000) |
| Rigorous / publication-quality | 2 000–5 000 |
| Noisy project with many tasks | 5 000+ |
The default of 1 000 is a good balance between ranking stability and
computational cost for most projects. If you reduce pass1_iterations below
100 the engine logs a warning that the criticality indices may be noisy.
pass1_iterations is automatically capped to --iterations if it is larger.
Interpreting the Delta¶
The delta values tell you the effect of smarter dispatch on each reported metric, holding all other inputs constant:
| Delta sign | Meaning |
|---|---|
Negative (e.g. -97 h) |
Pass-2 schedule is shorter → good, the reordering helped |
| Near zero | No contention, or all tasks equally critical → reordering made no difference |
| Positive | Unusual; could indicate that the pass-1 ranking was noisy (increase pass1_iterations) |
A large negative delta on mean/P80 combined with a large negative delta on resource wait time is strong evidence that the project suffers from avoidable resource contention, and that assigning more resources or restructuring task ownership would reduce schedule risk.
Related chapters¶
- Resource and Calendar Constrained Scheduling for resource setup, calendar configuration, and the full constrained scheduling walkthrough
- Running Simulations for the complete CLI reference
- Interpreting Results for understanding constrained diagnostics
\newpage