A simulation-based approach to optimizing ambulance staging locations in Manhattan, reducing EMS response times by 19% over a spatially-stratified baseline through demand-weighted allocation with capacity=2 constraints.
| Metric | P0 (Baseline) | P2 (Optimized) | Improvement |
|---|---|---|---|
| Mean Response Time | 3.17 min | 2.57 min | β18.9% |
| P95 Response Time | 6.26 min | 4.66 min | β25.6% |
| 8-min Coverage | 99.7% | 99.7% | +0.0 pp |
| Mean Utilization | 7.6% | 7.4% | β0.2 pp |
Results based on 2,700+ simulation experiments with 30 replications each (p < 0.001). Capacity sensitivity analysis (cap 1β5) confirms cap=2 as operationally optimal default. Alternative analyses confirm stability under distance metric choice and geographic focus.
This project evaluates three ambulance allocation policies for Manhattan's 48 FDNY firehouses:
- P0 (Spatially-Stratified Uniform): Latitude-based geographic stratification providing even northβsouth coverage
- P1 (Demand-Proportional): Units allocated based on nearby demand
- P2 (Demand-Weighted Optimized): MIP-optimized allocation minimizing expected response time
The analysis combines:
- NHPP Demand Modeling β Calibrated from 2.24M historical motor vehicle collision records
- Mixed-Integer Programming β Three optimization formulations with capacity=2 constraint (PuLP/CBC solver)
- Discrete-Event Simulation β SimPy-based DES with 2,700+ production runs across 9 experiment sets
- Statistical Analysis β ANOVA, Tukey HSD, confidence intervals, effect sizes
- Capacity Sensitivity Analysis β Full spectrum (cap 1β5) across 9 fleet sizes
- Alternative Analyses β Manhattan distance metric comparison + CBD-focused optimization evaluation
If you're new to the project:
- Read the Technical Report Β§1 (Executive Summary)
- Browse
results/baseline/β this is the canonical source of truth - Run the baseline pipeline (optional β results are pre-committed):
python scripts/run_production_v2.py
- Understand the tier system: Canonical β Supporting β Historical (see below)
For supporting analyses: results/analysis/ and docs/analysis/
For architecture deep-dive: docs/core/ARCHITECTURAL_MAP.md
For artifact governance: docs/core/artifact_governance_map.md
All results and documentation follow a three-tier governance system:
| Tier | Label | Location | Status |
|---|---|---|---|
| Canonical | β Current | results/baseline/, docs/core/ |
Authoritative β cited in technical report and README |
| Supporting | π¬ Analysis | results/analysis/, docs/analysis/ |
Robustness and sensitivity analyses that strengthen canonical findings |
| Historical | results/archive/, docs/archive/, scripts/archive/ |
Superseded artifacts (e.g., cap=5 era). Retained for audit trail only |
Canonical values used throughout this project:
| Parameter | Value | Source |
|---|---|---|
| Base arrival rate (Ξ»β) | 3.48 crashes/hour | configs/demand.yaml |
| Firehouse capacity (C) | 2 units/firehouse | configs/optimization.yaml, DEC-010 |
| Simulation horizon | 168 hours (1 week) | configs/simulation.yaml |
| Replications per scenario | 30 | configs/simulation.yaml |
| Master seed | 42 | configs/demand.yaml |
| Coverage threshold | 8 minutes (NFPA) / 6 minutes (NYC) | configs/simulation.yaml |
| Average EMS speed | 20 mph | configs/service.yaml |
| Service time mean | 25 min (LogNormal) | configs/service.yaml |
| Canonical K set | {10, 15, 20, 25, 30, 35, 40, 45, 48} | configs/optimization.yaml |
Full governance details:
docs/core/artifact_governance_map.mdFigure/table provenance:docs/core/figure_table_traceability.md
The study comprises 4 core experiments and 4 supporting analyses, totalling 2,700+ simulation runs:
| Experiment | Factors | Scope | Generator |
|---|---|---|---|
| Exp1 β Policy Comparison | P0, P1, P2 at K=20 | 3 Γ 30 reps = 90 runs | scripts/run_production_v2.py |
| Exp2 β Fleet Sensitivity | 9 K-values Γ 3 policies | 27 Γ 30 reps = 810 runs | scripts/run_production_v2.py |
| Exp3 β Demand Sensitivity | 6 demand multipliers Γ 2 policies | 12 Γ 30 reps = 360 runs | scripts/run_production_v2.py |
| Exp4 β Service Robustness | 3 service means Γ 2 policies | 6 Γ 30 reps = 180 runs | scripts/run_production_v2.py |
| Capacity β Cap Sensitivity | cap {1β5} Γ K {20,40} Γ 3 policies | Supporting | scripts/analysis/capacity_sensitivity_*.py |
| CBD β CBD Robustness | CBD surge scenarios | 330 runs | scripts/analysis/run_cbd_experiment.py |
| Distance β Metric Comparison | Haversine vs Manhattan | Supporting | scripts/analysis/run_distance_comparison_experiment.py |
| Queue/Seasonal β Derived | Post-hoc analysis of Exp1β4 | Supporting | scripts/analysis/analyze_queue_metrics.py, analyze_seasonal_patterns.py |
All experiments use Common Random Numbers (CRN) for variance reduction. Verification (4 tests) and validation (3 pilots) are documented in the technical report Β§4.4.3.
Full experimental design:
docs/core/experimental_design.md
The repository is organized to eliminate ambiguity:
Results:
- Baseline β canonical results (
results/baseline/) - Analysis β robustness & sensitivity (
results/analysis/) - Archive β historical artifacts (
results/archive/)
Scripts:
- Scripts (root) β production entry points
- scripts/analysis/ β supporting analyses
- scripts/archive/ β legacy utilities
Documentation:
- docs/core/ β primary documentation
- docs/analysis/ β supporting analyses
- docs/archive/ β historical records
ems-optimization/
βββ configs/ # YAML configuration files
β βββ demand.yaml # NHPP demand model parameters
β βββ optimization.yaml # MIP solver settings (capacity=2 default)
β βββ service.yaml # Travel & service time config
β βββ simulation.yaml # DES engine settings
β βββ cbd_scenario.yaml # CBD robustness experiment config
βββ data/
β βββ raw/ # Original data files (NYC Open Data)
β βββ processed/ # Generated data (not in Git β run `make data`)
β βββ manifests/ # Data audit records
βββ docs/ # Project documentation (62+ .md files, see docs/core/DOCUMENTATION_INDEX.md)
β βββ core/ # Key deliverables, specs, guides
β β βββ technical_report.md # Full final report (v2.1.0)
β β βββ executive_presentation.md # Stakeholder slide deck
β β βββ ... # Additional core docs
β βββ analysis/ # Robustness & sensitivity analyses
β β βββ capacity_sensitivity_analysis.md
β β βββ ... # Additional analysis docs
β βββ archive/ # Historical/superseded docs
βββ notebooks/ # Jupyter analysis notebooks
β βββ 02_eda_spatiotemporal.ipynb
β βββ 03_input_modeling.ipynb
β βββ 04_service_travel_proxy.ipynb
β βββ 05_optimization.ipynb
β βββ 06_simulation_debug.ipynb
β βββ 07_production_results.ipynb
β βββ 08_statistical_analysis.ipynb
β βββ 09_cbd_analysis.ipynb
β βββ colab_standalone/ # Google Colab standalone notebooks
β βββ individual/ # 8 phase notebooks (00β07)
β βββ EMS_Optimization_Complete_Pipeline.ipynb
β βββ README.md
βββ results/
β βββ baseline/ # Canonical production results (cap=2)
β β βββ allocations/ # Policy allocation CSVs
β β βββ figures/ # Publication-ready figures
β β βββ simulation/ # Verification & validation output
β β βββ tables/ # Statistical summary tables
β βββ analysis/ # Supporting robustness & sensitivity
β β βββ capacity_comparison/ # Capacity sensitivity (cap 1β5) results
β β βββ cbd_focused_comparison/ # CBD-focused vs Manhattan-wide results
β β βββ distance_comparison/ # Haversine vs Manhattan metric results
β β βββ heatmaps/ # Allocation heatmaps (K Γ cap Γ policy)
β β βββ maps/ # Allocation map visualizations
β β βββ simulation/ # CBD & production DES output
β β βββ tables/ # Analysis summary tables
β βββ archive/ # Legacy/historical artifacts (cap=5 era)
β β βββ figures/ # Superseded figures
β β βββ optimization/ # Phase 3 historical optimization results
β β βββ tables/ # Superseded tables
β βββ figures/ # Top-level visualization PNGs
β βββ tables/ # Top-level statistical tables (CSV + LaTeX)
β βββ simulation/ # Top-level simulation output data
βββ scripts/ # Automation & analysis scripts (23+)
βββ src/ems_readiness/ # Core Python package (v0.6.0)
β βββ demand/ # NHPP arrival generator
β βββ service/ # Travel time & service time models
β βββ optimization/ # MIP formulations, policies & allocator
β βββ simulation/ # SimPy DES engine
β βββ utils/ # Distance calculations
βββ tests/ # 176 unit tests (pytest)
βββ requirements.txt # Python dependencies
βββ Makefile # Build automation
βββ LICENSE # MIT License
- Python 3.11+
- pip
# Clone the repository
git clone https://github.com/cnsp/ems-optimization.git
cd ems-optimization
# Create virtual environment and install dependencies
make setup
# Or manually:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Install project package in development mode
pip install -e .Large raw data files are tracked via Git LFS. Ensure Git LFS is installed before cloning:
git lfs install # one-time setup
git clone https://github.com/cnsp/ems-optimization.gitThe following files are pulled automatically via LFS on clone:
data/raw/Motor_Vehicle_Collisions_-_Crashes_20260223.csv(536 MB)data/raw/Police_Precincts_20260223.csv(3.6 MB)
All other raw files (boundary GeoJSONs, firehouse listings, data dictionaries) and key processed files (distance matrices, demand lambdas, firehouse lists) are tracked directly in Git.
If LFS files were not pulled (e.g., shallow clone), run:
git lfs pullSee data/raw/README.md for original NYC Open Data download links.
Note: The Colab notebooks (
notebooks/colab_standalone/) include self-contained data processing cells that generatecrashes_manhattan.csvfrom the raw crash CSV on first run. All other processed files are tracked in Git and require no extra setup.
Generate all processed data (single command):
make data
# Or: python scripts/generate_all_data.py- Verify data:
make verify-dataThe data pipeline (scripts/generate_all_data.py) supports several options:
| Flag | Description |
|---|---|
--force |
Regenerate all files even if they exist |
--no-validate |
Skip raw data validation checks |
--no-cache |
Disable smart caching |
--jobs N / -j N |
Parallel workers (0 = auto-detect CPU count) |
--tier {1,2,3} |
Run only a specific tier |
--version |
Show current data version info |
--seed N |
Master seed for reproducibility (default: 42) |
--verify |
Check data existence without generating |
# Force-regenerate with 4 parallel workers
python scripts/generate_all_data.py --force --jobs 4
# Show data version/lineage info
python scripts/generate_all_data.py --version
# Skip validation (faster startup)
python scripts/generate_all_data.py --no-validateNote: Notebooks auto-detect and regenerate missing processed data on first run, so you can also just open any notebook and it will trigger the pipeline if needed.
# 1. Process data (if raw data available)
make data
# 2. Run optimization comparison
python scripts/analysis/run_optimization_comparison.py
# 3. Run simulation verification
python scripts/run_verification.py
# 4. Run validation pilots
python scripts/run_validation_pilots.py
# 5. Run production experiments (1,440 simulations β takes ~2-4 hours)
python scripts/run_production_v2.py
# 6. Analyze results and generate figures
python scripts/analysis/analyze_production_results.py
python scripts/analysis/generate_publication_figures.py
# 7. Run CBD robustness experiment (330 simulations)
python scripts/analysis/run_cbd_experiment.py --reps 30
# 8. Run gap closure analyses
python scripts/analysis/analyze_queue_metrics.py
python scripts/analysis/analyze_seasonal_patterns.py
# 9. Run alternative analyses (Phase 8)
python scripts/analysis/generate_manhattan_distance_matrix.py
python scripts/analysis/run_distance_comparison_experiment.py --reps 10
python scripts/analysis/run_cbd_focused_optimization.py --reps 10
# 10. Run capacity sensitivity analysis (Phase 9)
python scripts/analysis/capacity_sensitivity_full_spectrum.py
# 11. Run P0 spatial stratification analysis
python scripts/analysis/p0_spatial_analysis.py
# 12. Run extended fleet analysis experiments (810 simulations β cap=2)
python scripts/run_production_v2.pyfrom ems_readiness.simulation import EMSSimulation, BatchRunner
from ems_readiness.optimization import EMSAllocator
# Initialize allocator
allocator = EMSAllocator.from_project(".")
# Get P2 allocation for K=20 units (capacity=2 default)
result = allocator.solve(model="demand_weighted", K=20)
print(result.allocation)
# Run simulation
sim = EMSSimulation.from_config("configs/simulation.yaml", allocation=result.allocation)
metrics = sim.run(duration_hours=168)
print(f"Mean RT: {metrics['mean_response_time']:.2f} min")# Run all 176 tests
pytest tests/ -v
# Run specific test modules
pytest tests/test_simulation_core.py -v
pytest tests/test_dispatch_logic.py -v
pytest tests/test_extreme_cases.py -v
pytest tests/test_reproducibility.py -vπ Full index:
docs/core/DOCUMENTATION_INDEX.mdβ master list of all 62 docs with status badges (β Current / π Historical / π Reference)
| Document | Description |
|---|---|
| Technical Report | Full final report (v2.1.0) with all findings |
| Executive Summary | One-page executive summary |
| Executive Presentation | 10-slide stakeholder presentation |
| Data Usage Guide | Which data/results files to use and why |
| Reproducibility Guide | How to reproduce all results |
| Document | Description |
|---|---|
| Conceptual Model | DES model specification |
| Optimization Formulation | MIP mathematical formulations |
| Experimental Design | Factorial experiment specification |
| Optimization Results | Current optimization results (cap=2) |
| Document | Description |
|---|---|
| Capacity Sensitivity | Full-spectrum capacity (cap 1β5) analysis |
| CBD Robustness | CBD-specific DES experiment findings |
| Distance Metric Comparison | Haversine vs. Manhattan distance analysis |
| Research Questions | Evaluation of all 5 research questions |
| Document | Description |
|---|---|
| Code Documentation | Architecture and API guide |
| Notebook Guide | Notebook descriptions and execution order |
| Testing Guide | Testing framework and how to run tests |
| Figure Trace Guide | Figure-to-source traceability |
| Visualization Index | Complete catalog of all generated figures |
The results/ folder contains output from multiple project phases. Each subfolder has its own README.md explaining what's there and whether it's current or legacy.
| Folder | Label | Status | What's Inside |
|---|---|---|---|
results/baseline/ |
Canonical | β Current | Production allocations, figures, simulation verification/validation, tables |
results/baseline/simulation/ |
Canonical | β Current | Verification tests, validation pilots |
results/analysis/ |
Supporting | β Current | All robustness & sensitivity analyses |
results/analysis/capacity_comparison/ |
Supporting | β Current | Full capacity sweep (cap 1β5) |
results/analysis/cbd_focused_comparison/ |
Supporting | β Current | CBD-focused optimization experiment |
results/analysis/distance_comparison/ |
Supporting | β Current | Manhattan vs Haversine comparison |
results/analysis/heatmaps/ |
Supporting | β Current | Allocation heatmaps across K Γ cap Γ policy |
results/analysis/maps/ |
Supporting | β Current | Allocation maps at K=40 |
results/archive/ |
Legacy | Phase 3 historical artifacts (cap=5 era) | |
results/archive/optimization/ |
Legacy | Superseded optimization results | |
results/figures/ |
β | β Mostly Current | Publication figures (pub_fig*) + technical plots |
results/tables/ |
β | β Mostly Current | Publication tables + experiment pivots (see README) |
results/simulation/ |
β | β Current | Top-level simulation output data |
Quick reference: results/WHICH_FILES_TO_USE.md
Full guide: docs/core/data_usage_guide.md
For external verification and reproduction, standalone Google Colab notebooks are available in notebooks/colab_standalone/. These require no local setup β just open in Colab and run.
| Notebook | Phase | Runtime |
|---|---|---|
00_colab_setup_and_data |
Environment setup, data verification | ~2 min |
01_colab_eda_spatiotemporal |
Exploratory data analysis | ~5 min |
02_colab_demand_modeling |
NHPP demand model | ~5 min |
03_colab_service_modeling |
Travel/service time models | ~3 min |
04_colab_optimization |
P0/P1/P2 allocation policies | ~5 min |
05_colab_simulation |
SimPy DES (demo or full-scale) | 5 min β 8 hr |
06_colab_statistical_analysis |
ANOVA, effect sizes, CIs | ~3 min |
07_colab_visualization_reporting |
Publication figures, summary report | ~3 min |
| All-in-One Pipeline | Complete end-to-end analysis | 5β10 hr |
See notebooks/colab_standalone/README.md for detailed instructions.
- RQ1: How does demand for EMS services vary spatially and temporally across Manhattan?
- RQ2: What is the optimal allocation of K ambulances to minimize expected response time?
- RQ3: How do optimized allocations compare to the spatially-stratified uniform baseline under realistic conditions?
- RQ4: How sensitive are policy rankings to fleet size, demand intensity, service time, and firehouse capacity?
- RQ5: What fleet size achieves β₯95% coverage within 8 minutes?
@misc{ems_optimization_2026,
title={EMS Readiness Optimization for Manhattan: A Simulation-Based Approach to Ambulance Staging},
author={EMS Optimization Research Team},
year={2026},
howpublished={\url{https://github.com/cnsp/ems-optimization}},
note={Version 1.3.0}
}This project is licensed under the MIT License β see LICENSE for details.
- Repository: github.com/cnsp/ems-optimization
- Issues: GitHub Issues
Built with Python, SimPy, PuLP, pandas, and matplotlib. 8,500+ lines of code across 14 modules, tested with 176 unit tests and 2,700+ simulation experiments. Covers capacity sensitivity (cap 1β5), spatially-stratified P0 baseline, and alternative distance metric and CBD-focused optimization analyses.