ResQ-Graph is an agent-based graph simulation for emergency ambulance dispatch. It models a fleet of ambulances navigating a city road network (represented as a graph) to respond to dynamically generated emergency events. The project aims to simulate, visualize, and optimize response times using smart dispatch algorithms, dynamic fleet rebalancing, and realistic traffic models.
This repository is currently up-to-date through Sprint 10.
The system relies on a central tick-based simulation engine that orchestrates several distinct sub-systems.
The heart of the project. It runs a while loop (capped at a defined number of ticks), updating the state of ambulances, spawning new events, updating traffic congestion, calling the dispatcher, and triggering the Pygame renderer. It ensures deterministic state updates.
- Map: The road network is loaded from
data/model_town.graphml(converted to an undirected MultiGraph to prevent dead-end trapping). - A* Pathfinding:
astar.pycalculates the shortest physical route for ambulances using a Haversine heuristic. - Distance Matrix: To avoid running A* for every single idle ambulance when a new event occurs, the system pre-computes an O(1) all-pairs shortest path distance matrix (
data/distance_matrix.npy).
Introduced in Sprint 7, this model simulates dynamic edge congestion based on local event density and a base background level. It modifies the edge weights in the graph, affecting A* pathfinding and triggering mid-route rerouting if conditions worsen significantly (CONGESTION_DETECTED).
Acts as the central command. It owns the queue of active_events. When a new emergency is ingested, it checks for idle ambulances and uses assign_nearest_idle() to find the closest one via the distance_matrix. It also coordinates periodic fleet rebalancing (sending idle ambulances to detected hotspots).
Uses a custom, from-scratch implementation of HDBSCAN (hdbscan.py) to detect demand hotspots from active event locations. The DemandClusterer groups events and finds optimal centroid nodes for proactive ambulance positioning (REBALANCING).
Simulates emergencies using a Poisson distribution. At each tick, it has a probability to spawn an Accident at a random node on the graph.
State machines that cycle between IDLE, IN_TRANSIT, ON_SCENE, and REBALANCING. They follow pre-calculated paths node-by-node, check for worsening traffic conditions to trigger rerouting, and interpolate their pixel positions between nodes for smooth rendering.
Logs response times (Spawn Tick → Arrival Tick) for every event. It provides real-time stats to the HUD, exports metrics_events.csv and metrics_summary.csv when the simulation ends, and maintains a comprehensive multi-level sim.log.
A highly optimized Pygame visualizer that renders:
- The static map background
-- Congestion heatmaps (Layered cached surface,
Tto toggle) -- Hotspot pulsing circles and convex hulls (Hto toggle) -- Ambulance sprites (color-coded by state) -- Emergency locations (Red Xs) -- Dynamic dashed polylines representing active routes -- A real-time HUD, a detailed metrics overlay (M), and a full log history (L)
resq-graph/
├── data/
│ ├── model_town.graphml # Road network map data
│ ├── node_positions.json # Pre-calculated pixel coordinates for nodes
│ ├── map_bg.png # Visual map background
│ └── distance_matrix.npy # Precomputed distance matrix (auto-generated)
├── outputs/ # Generated CSV metrics, logs, and baseline results go here
│ ├── figures/ # Auto-generated matplotlib plots (Sprint 8)
│ ├── baseline_results.csv # Sprint 8: per-run baseline ART results
│ ├── baseline_report.md # Sprint 8: auto-generated analysis report
│ ├── random_fleet_log.json # Sprint 8: placement log for reproducibility
│ └── sensitivity/ # Sprint 10: sensitivity sweep results (CSV + Plots)
├── src/
│ ├── main.py # Entry point for the simulation
│ ├── config.py # Hyperparameters, paths, and visual constants
│ ├── sim_config_loader.py # YAML configuration parser
│ ├── run_baseline.py # Sprint 8: headless batch baseline runner
│ ├── analyze_baseline.py # Sprint 8: analysis & report generator
│ ├── run_ai_fleet.py # Sprint 9: GA fleet runner
│ ├── run_sensitivity.py # Sprint 10: headless sensitivity sweep runner
│ ├── analyze_sensitivity.py # Sprint 10: sensitivity report generator
│ ├── astar.py # Core A* navigation algorithm
│ ├── distance_matrix.py # Distance matrix computation script
│ ├── intelligence/
│ │ ├── hdbscan.py # Custom HDBSCAN implementation
│ │ └── demand_clustering.py # Hotspot detection logic
│ ├── rendering/
│ │ ├── pygame_renderer.py # Pygame visualization logic
│ │ └── visualizer.py # Matplotlib visualizer + Sprint 8 baseline plots
│ └── simulation/
│ ├── ambulance.py # Ambulance class & state management
│ ├── assignment.py # O(1) assignment and tie-breaking logic
│ ├── dispatcher.py # Centralized dispatcher orchestrator
│ ├── event_spawner.py # Poisson emergency generation
│ ├── metrics_tracker.py # Data tracking & CSV export
│ ├── random_fleet.py # Sprint 8: random station placement generator
│ ├── sim_logger.py # Multi-level logging configuration
│ ├── simulation_engine.py # Main tick loop
│ └── traffic.py # Dynamic traffic congestion model
├── tests/ # Comprehensive Pytest test suite
├── sim_config.yaml # Centralized simulation parameter configuration
├── headless_baseline.yaml # Sprint 8: reproducible headless batch config
├── headless_ai.yaml # Sprint 9: reproducible AI fleet config
└── headless_sensitivity.yaml # Sprint 10: parameter sweep configuration
Ensure you have a virtual environment set up with pygame, networkx, numpy, pyyaml, and pytest installed.
Run the simulation:
python src/main.py- Press Spacebar to pause/resume.
- Press M to toggle the detailed metrics panel.
- Press H to toggle the hotspot overlay.
- Press T to toggle the traffic congestion overlay.
- Press L to toggle the full log history overlay.
- Press + / - to adjust the live Poisson event rate (Lambda).
- Press A to inject a new ambulance into the active simulation.
- Press K to trigger a manual HDBSCAN hotspot rebalance.
Run headless (no window):
python src/main.py --headlessRun with specific profile:
You can modify sim_config.yaml to change parameters or run headless.
Run the test suite:
pytest tests/ -v- Install dependencies into your virtual environment:
pip install -r requirements.txt- Run the baseline batch experiment (headless, 10 runs x 1000 ticks):
python src/run_baseline.py --headless --config headless_baseline.yaml- Analyze results and generate the report + figures:
python src/analyze_baseline.py- View the report:
outputs/baseline_report.md
Seeds are defined in headless_baseline.yaml:
random_seed— controls random fleet station placementevent_seed— controls Poisson event spawningambulance_seed— reserved for future randomized ambulance init
Changing any seed will change results. The random_fleet_log.json file
records every placement set for full reproducibility auditing.
Sprint 9 introduced the AI vs Baseline comparison, including a Traffic-Aware GA and a Return to Base fleet policy, resulting in a highly significant improvement.
To reproduce the AI comparison:
- Run the Baseline (10 runs, random stations):
python src/run_baseline.py --headless --config headless_baseline.yaml- Run the AI Fleet (10 runs, GA-optimized stations):
python src/run_ai_fleet.py --headless --config headless_ai.yaml- Generate the Comparison Report:
python src/analyze_comparison.py- Baseline ART: ~25.7 ticks
- AI Fleet ART: ~23.1 ticks
- Improvement: +10.3%
- Statistical Significance: Yes (p < 0.01)
Sprint 10 introduced a robust framework for sweeping simulation parameters to find optimal operational points.
To reproduce the Sensitivity Analysis:
- Run the headless sweep (this handles Lambda, Fleet Size, and HDBSCAN parameters):
python src/run_sensitivity.py --headless --config headless_sensitivity.yaml- Generate the report and visualizations:
python src/analyze_sensitivity.py- View the results:
outputs/sensitivity_report.md
Through our comprehensive 215-run sweep, we discovered exactly how the ResQ-Graph system handles different "stress levels":
- Event Rate (Lambda): The AI fleet is consistently ~10% faster than the random baseline during normal city demand. However, when the city is overwhelmed (very high accident rates), both fleets eventually slow down as ambulances become fully utilized.
- Fleet Scaling: We found a "sweet spot" at 7 ambulances. Increasing from 3 to 5 ambulances cuts response times by nearly 40%, but adding more than 7 ambulances provides significantly smaller gains.
- Optimal Intelligence: Our most efficient cluster settings were found to be
min_cluster_size=5andmin_samples=5. Running a rebalance every 25 ticks (rather than 50 or 100) proved most effective at keeping ambulances near predicted hotspots.
| Setting | Value | Why? |
|---|---|---|
| Best HDBSCAN Cluster Size | 5 | Balances precision with enough sample size. |
| Best Rebalance Interval | 25 Ticks | Keeps the fleet "agile" and responsive to new data. |
| Recommended Fleet Size | 7 | Best performance-to-cost ratio for this map. |
When running batch experiments, you will see logs like:
Run 0 | seed=42 | events= 49 | ART= 22.86
- Ticks: The fundamental unit of time in the simulation. The simulation engine runs step-by-step in a loop. During one tick, an ambulance moves one unit of distance, traffic decays, and the event spawner rolls the dice to see if a new accident happens.
- Seed: A number used to initialize the Random Number Generator. By using different seeds (e.g., 42, 43, 44...), we force the simulation to generate a completely different sequence of accidents (different times, different locations) for each run. This ensures we are testing the fleet across 10 completely different "days" of emergencies, proving the AI is consistently better, not just lucky.
- Events: The total number of accidents that were spawned during that specific run. Because accidents spawn based on probability (Poisson distribution), the exact number of events varies from run to run.
If you are an AI reading this to write future code for this project:
- Rendering:
pygame_renderer.pyrelies strictly on data passed to itsdraw()method. Do not put simulation state mutations inside the renderer. - Distance Matrix: Never bypass
assignment.pyfor calculating proximity. Always use the distance matrix for O(1) lookups to keep the simulation performant, falling back to Euclidean distance only if explicitly necessary. - Dependencies: The graph is loaded as
nx.MultiGraph(G)(undirected) to prevent ambulances from getting stuck in directed dead-ends. - HDBSCAN: The custom implementation in
src/intelligence/hdbscan.pyis fully tested and robust. It uses an Excess of Mass formula(birth_level - death_level) * sizefor cluster stability extraction. - Seeds: All randomness must flow through
sim_config_loader.py. Zero hardcoded seeds. Useevent_seed,random_seed, andambulance_seedfrom config. - Headless mode: Set
SDL_VIDEODRIVER=dummyBEFORE anypygameimport. This is enforced inmain.pyandrun_baseline.py.