Recipe 14.8: Python Implementation Example
Heads up: This is a deliberately simplified, illustrative implementation of the ambulance routing and dispatch optimization from Recipe 14.8. It demonstrates the core concepts (fleet state management, candidate scoring with coverage awareness, hospital destination selection, and background repositioning optimization) using a combination of boto3 and Google OR-Tools. It is not production-ready. The fleet is tiny, travel times are synthetic, and there's no real GPS stream or CAD integration. Think of it as the whiteboard sketch that helps you understand how the dispatch scoring and coverage optimization actually work under the hood. A starting point, not a destination.
The main recipe uses Kinesis for GPS ingestion, ElastiCache for travel time caching, and SageMaker for demand forecasting. This example runs everything locally with in-memory state and a simple grid-based coverage model. The optimization math is identical; the infrastructure is stripped away so you can focus on the decision logic.
Setup
You'll need the optimization solver and AWS SDK installed:
pip install boto3 ortools
ortools is Google's open-source optimization suite. We use the linear solver (SCIP backend) for the repositioning coverage problem, which is formulated as a set-covering assignment. It's free, actively maintained, and handles the scale of a typical metro EMS fleet (20-60 units, 50-200 demand zones) in seconds.
Your environment needs AWS credentials configured (via environment variables, instance profile, or ~/.aws/credentials). The IAM role or user needs:
dynamodb:PutItemdynamodb:GetItemdynamodb:Querygeo:CalculateRouteMatrix(if you swap in real Location Service calls)
For the full pipeline (with Kinesis GPS ingestion, ElastiCache, SageMaker demand forecasting, and Step Functions orchestration), you'd need additional permissions, but this example keeps the focus on the dispatch and coverage optimization logic.
Configuration and Constants
import json import math import logging import datetime from datetime import timezone from decimal import Decimal from typing import Optional import boto3 from botocore.config import Config from ortools.linear_solver import pywraplp logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) BOTO3_RETRY_CONFIG = Config(retries={"max_attempts": 3, "mode": "adaptive"}) dynamodb = boto3.resource("dynamodb", config=BOTO3_RETRY_CONFIG) # Table where we store dispatch decisions for audit trail. DISPATCH_TABLE_NAME = "ems-dispatch-decisions" # Response time targets (seconds). These come from NFPA 1710 and local # medical direction protocols. Priority 1 = life-threatening emergency. RESPONSE_TIME_TARGETS = { 1: 480, # 8 minutes for Priority 1 (cardiac arrest, major trauma) 2: 600, # 10 minutes for Priority 2 (chest pain, difficulty breathing) 3: 900, # 15 minutes for Priority 3 (falls, minor injuries) 4: 1200, # 20 minutes for Priority 4 (non-emergency transports) 5: 1800, # 30 minutes for Priority 5 (scheduled transfers) } # Unit capability levels. ALS units carry paramedics with cardiac monitors, # intubation equipment, and IV medications. BLS units have EMTs with basic # interventions. You cannot send a BLS unit to an ALS-required call. CAPABILITY_LEVELS = { "ALS": 2, # Advanced Life Support (paramedic) "BLS": 1, # Basic Life Support (EMT) } # Scoring weights for the dispatch optimizer. These control the trade-off # between "send the fastest unit" and "preserve system coverage." # Tuned per system based on historical performance analysis. DISPATCH_WEIGHTS = { "response_time": 0.60, # dominant factor: get there fast "coverage_impact": 0.25, # don't leave zones uncovered "fatigue": 0.10, # crew welfare (hours on shift) "workload": 0.05, # balance calls across crews } # For Priority 1 calls, we override the weights to prioritize pure speed. # Coverage concerns take a back seat when someone is dying. PRIORITY_1_WEIGHTS = { "response_time": 0.90, "coverage_impact": 0.10, "fatigue": 0.0, "workload": 0.0, } # Coverage threshold: a zone is "covered" if at least one available unit # can reach its centroid within this many seconds. COVERAGE_THRESHOLD_SECONDS = 480 # 8 minutes # Maximum repositioning moves per optimization cycle. Moving too many units # at once creates chaos and dispatcher confusion. MAX_REPOSITION_MOVES = 3 # Average ambulance speed for travel time estimation (km/h). # Emergency response (lights and sirens) vs. non-emergency. SPEED_EMERGENCY_KMH = 55 # urban average with L&S SPEED_NORMAL_KMH = 35 # urban average without L&S
Step 1: Fleet State Management
The pseudocode calls this process_gps_update. In production, GPS events flow through Kinesis into a Lambda that updates DynamoDB. Here we model the fleet state in memory and show how you'd write it to DynamoDB for persistence.
def build_initial_fleet(): """ Create a synthetic fleet for demonstration. In production, this state lives in DynamoDB and is updated by the GPS ingestion pipeline. Each unit has: - unit_id: unique identifier (matches the MDT/radio callsign) - capability: ALS or BLS - latitude/longitude: current GPS position - status: AVAILABLE, DISPATCHED, EN_ROUTE, ON_SCENE, TRANSPORTING, AT_HOSPITAL - zone: which coverage zone the unit is currently in - shift_start: when this crew's shift began (for fatigue scoring) - calls_today: how many calls this crew has run (for workload balancing) """ fleet = [ { "unit_id": "MEDIC-1", "capability": "ALS", "latitude": 38.9072, "longitude": -77.0369, "status": "AVAILABLE", "zone": "ZONE-A", "shift_start": "2026-06-01T06:00:00Z", "calls_today": 3, }, { "unit_id": "MEDIC-3", "capability": "ALS", "latitude": 38.9200, "longitude": -77.0200, "status": "AVAILABLE", "zone": "ZONE-B", "shift_start": "2026-06-01T06:00:00Z", "calls_today": 5, }, { "unit_id": "MEDIC-5", "capability": "ALS", "latitude": 38.8950, "longitude": -77.0500, "status": "EN_ROUTE", "zone": "ZONE-C", "shift_start": "2026-06-01T06:00:00Z", "calls_today": 4, }, { "unit_id": "BLS-2", "capability": "BLS", "latitude": 38.9100, "longitude": -77.0450, "status": "AVAILABLE", "zone": "ZONE-A", "shift_start": "2026-06-01T18:00:00Z", "calls_today": 1, }, { "unit_id": "BLS-4", "capability": "BLS", "latitude": 38.9300, "longitude": -77.0100, "status": "AVAILABLE", "zone": "ZONE-D", "shift_start": "2026-06-01T18:00:00Z", "calls_today": 2, }, ] return fleet def get_available_units(fleet, required_capability="BLS"): """ Filter fleet to units that are available and meet the capability requirement. ALS units can handle both ALS and BLS calls (paramedics can do everything EMTs can do, plus more). BLS units can only handle BLS calls. """ available = [] for unit in fleet: if unit["status"] != "AVAILABLE": continue # ALS units satisfy any capability requirement. # BLS units only satisfy BLS requirements. unit_level = CAPABILITY_LEVELS[unit["capability"]] required_level = CAPABILITY_LEVELS[required_capability] if unit_level >= required_level: available.append(unit) return available def validate_gps_fix(gps_event, previous_fix=None, service_area_bounds=None): """ Validate a GPS position fix before accepting it into fleet state. In production, these checks run in the Kinesis consumer Lambda before the DynamoDB write. Invalid fixes are logged to a GPS anomalies table for fleet maintenance review but do not update the unit's position. Args: gps_event: dict with unit_id, latitude, longitude, timestamp, speed previous_fix: the last accepted fix for this unit (for movement checks) service_area_bounds: dict with min_lat, max_lat, min_lon, max_lon Returns: (is_valid, reason) tuple. If invalid, reason explains why. """ lat = gps_event.get("latitude", 0) lon = gps_event.get("longitude", 0) # Check for GPS default/null coordinates (common when device loses lock) if lat == 0 and lon == 0: return False, "null_coordinates" # Bounds check: must be within service area (with buffer for mutual aid) if service_area_bounds: buffer = 0.5 # ~55 km buffer for mutual aid runs if not (service_area_bounds["min_lat"] - buffer <= lat <= service_area_bounds["max_lat"] + buffer): return False, "latitude_out_of_bounds" if not (service_area_bounds["min_lon"] - buffer <= lon <= service_area_bounds["max_lon"] + buffer): return False, "longitude_out_of_bounds" # Speed plausibility: ambulances do not exceed 200 km/h if gps_event.get("speed", 0) > 200: return False, "speed_implausible" # Movement plausibility: check against previous fix if previous_fix: distance_km = haversine_distance_km( previous_fix["latitude"], previous_fix["longitude"], lat, lon ) time_delta_s = ( gps_event["timestamp"] - previous_fix["timestamp"] ).total_seconds() if time_delta_s > 0 and distance_km / (time_delta_s / 3600) > 200: return False, "impossible_movement" return True, "ok"
Step 2: Travel Time Estimation
The main recipe uses Amazon Location Service for real road-network travel times with traffic. Here we use the Haversine formula with a speed estimate as a stand-in. In production, you'd call Location Service's CalculateRouteMatrix API and cache results in ElastiCache.
def haversine_distance_km(lat1, lon1, lat2, lon2): """ Calculate straight-line distance between two GPS coordinates in kilometers. This is the "as the crow flies" distance. Real road distance is typically 1.3x to 1.6x longer in urban areas (the "circuity factor"). """ R = 6371 # Earth's radius in km dlat = math.radians(lat2 - lat1) dlon = math.radians(lon2 - lon1) a = ( math.sin(dlat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2 ) c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) return R * c def estimate_travel_time_seconds(origin_lat, origin_lon, dest_lat, dest_lon, emergency=True): """ Estimate travel time in seconds between two points. In production, replace this with a call to Amazon Location Service: client.calculate_route( CalculatorName="ems-router", DeparturePosition=[origin_lon, origin_lat], DestinationPosition=[dest_lon, dest_lat], TravelMode="Car", DepartNow=True ) The Location Service response includes DurationSeconds accounting for real-time traffic. You'd then apply an emergency vehicle multiplier (typically 0.7x to 0.8x of civilian travel time for L&S response). For this example, we use Haversine distance with a circuity factor and speed estimate. It's wrong, but it's wrong in a predictable way that lets you see the optimization logic clearly. """ straight_line_km = haversine_distance_km( origin_lat, origin_lon, dest_lat, dest_lon ) # Urban circuity factor: roads aren't straight lines. # 1.4 is typical for grid-pattern cities. Winding suburbs might be 1.6+. road_distance_km = straight_line_km * 1.4 speed_kmh = SPEED_EMERGENCY_KMH if emergency else SPEED_NORMAL_KMH travel_time_hours = road_distance_km / speed_kmh travel_time_seconds = travel_time_hours * 3600 return travel_time_seconds
Step 3: Dispatch Scoring
The pseudocode calls this score_candidates. This is the heart of the system: evaluating each available unit against a call and producing a ranked list. The scoring function balances response time against system-level concerns like coverage preservation and crew welfare.
def compute_coverage_impact(unit, fleet, demand_zones): """ Estimate how much system coverage degrades if we dispatch this unit. The idea: if this unit is the only one covering its zone, removing it creates a critical gap. If three other units can also cover that zone, removing this one barely matters. Returns a score from 0.0 (no impact) to 1.0 (critical gap created). """ zone = unit["zone"] # Count how many other available units can cover this zone other_units_in_zone = 0 for other in fleet: if other["unit_id"] == unit["unit_id"]: continue if other["status"] != "AVAILABLE": continue if other["zone"] == zone: other_units_in_zone += 1 # Also check adjacent units that could reach this zone within threshold # (simplified: in production, use actual travel times to zone centroid) zone_centroid = demand_zones.get(zone, {}).get("centroid") if zone_centroid: for other in fleet: if other["unit_id"] == unit["unit_id"]: continue if other["status"] != "AVAILABLE": continue if other["zone"] == zone: continue # already counted travel = estimate_travel_time_seconds( other["latitude"], other["longitude"], zone_centroid[0], zone_centroid[1], emergency=True, ) if travel <= COVERAGE_THRESHOLD_SECONDS: other_units_in_zone += 0.5 # partial credit for adjacent coverage # Convert to impact score: fewer backup units = higher impact if other_units_in_zone >= 2: return 0.1 # well-covered zone, minimal impact elif other_units_in_zone >= 1: return 0.4 # one backup, moderate concern else: return 0.9 # no backup, critical gap if we send this unit def normalize_value(value, max_value): """Normalize a value to 0.0-1.0 range. Clamps at 1.0.""" return min(value / max_value, 1.0) def score_candidates(call, fleet, demand_zones): """ Score all available, capable units for a given call. Args: call: dict with location, priority, required_capability, nature_code fleet: list of all unit dicts (current fleet state) demand_zones: dict of zone_id -> {centroid, demand_weight} Returns: List of candidate dicts sorted by score (ascending = best first). Each candidate includes unit_id, travel_time, score, and coverage_impact. """ required_cap = call.get("required_capability", "BLS") available = get_available_units(fleet, required_cap) if not available: logger.warning("No available units meet capability requirement: %s", required_cap) return [] # Select weight profile based on call priority weights = PRIORITY_1_WEIGHTS if call["priority"] == 1 else DISPATCH_WEIGHTS candidates = [] now = datetime.datetime.now(timezone.utc) for unit in available: # Travel time from unit's current position to call location travel_time = estimate_travel_time_seconds( unit["latitude"], unit["longitude"], call["location"]["lat"], call["location"]["lng"], emergency=True, ) # Coverage impact: what happens to system coverage if we send this unit? coverage_impact = compute_coverage_impact(unit, fleet, demand_zones) # Fatigue: hours on shift. Slight penalty after 10 hours. shift_start = datetime.datetime.fromisoformat( unit["shift_start"].replace("Z", "+00:00") ) hours_on_shift = (now - shift_start).total_seconds() / 3600 fatigue_penalty = max(0.0, (hours_on_shift - 10) * 0.1) # Workload: calls run today, normalized against a reasonable max workload_score = normalize_value(unit["calls_today"], 10) # Composite score (lower is better) score = ( weights["response_time"] * normalize_value(travel_time, 1200) + weights["coverage_impact"] * coverage_impact + weights["fatigue"] * normalize_value(fatigue_penalty, 1.0) + weights["workload"] * workload_score ) candidates.append({ "unit_id": unit["unit_id"], "capability": unit["capability"], "travel_time_seconds": round(travel_time), "coverage_impact": round(coverage_impact, 2), "score": round(score, 4), }) # Sort by score ascending (best candidate first) candidates.sort(key=lambda c: c["score"]) return candidates
Step 4: Hospital Destination Selection
The pseudocode calls this select_hospital. After the unit arrives on scene and assesses the patient, the system recommends the best destination hospital based on clinical needs, transport time, and current capacity.
def build_hospital_status(): """ Synthetic hospital status data. In production, this comes from hospital systems via EventBridge (diversion status, ED census, bed availability) and is stored in DynamoDB for fast lookup. """ hospitals = [ { "hospital_id": "HOSP-GWU", "name": "GW University Hospital", "location": {"lat": 38.9007, "lng": -77.0508}, "capabilities": ["cath_lab", "interventional_cardiology", "level_1_trauma", "stroke_center"], "ed_capacity": 45, "current_ed_census": 24, "diversion_status": "OPEN", # OPEN, CONDITIONAL, FULL_DIVERSION }, { "hospital_id": "HOSP-MEDSTAR", "name": "MedStar Washington Hospital Center", "location": {"lat": 38.9296, "lng": -77.0164}, "capabilities": ["cath_lab", "interventional_cardiology", "level_1_trauma", "stroke_center", "burn_center"], "ed_capacity": 60, "current_ed_census": 52, "diversion_status": "CONDITIONAL", }, { "hospital_id": "HOSP-SIBLEY", "name": "Sibley Memorial Hospital", "location": {"lat": 38.9378, "lng": -77.1052}, "capabilities": ["stroke_center", "general_surgery"], "ed_capacity": 30, "current_ed_census": 12, "diversion_status": "OPEN", }, ] return hospitals def select_hospital(patient_needs, unit_location, hospitals): """ Recommend the best destination hospital for a patient. Args: patient_needs: dict with required_capabilities (list), acuity_level (1-5) unit_location: dict with lat, lng (ambulance's current position) hospitals: list of hospital status dicts Returns: Best hospital dict with transport_time and score, or None if no eligible hospital found (which would be a serious system failure in a real deployment). """ eligible = [] for hospital in hospitals: # Hard filter: must have all required capabilities has_all_caps = all( cap in hospital["capabilities"] for cap in patient_needs["required_capabilities"] ) if not has_all_caps: continue # Hard filter: diversion status if hospital["diversion_status"] == "FULL_DIVERSION": continue if (hospital["diversion_status"] == "CONDITIONAL" and patient_needs["acuity_level"] > 2): # Conditional diversion: only accepting critical patients (acuity 1-2) continue # Transport time from unit's current location to hospital transport_seconds = estimate_travel_time_seconds( unit_location["lat"], unit_location["lng"], hospital["location"]["lat"], hospital["location"]["lng"], emergency=True, ) # Capacity score: how full is the ED? (0.0 = empty, 1.0 = at capacity) capacity_ratio = hospital["current_ed_census"] / hospital["ed_capacity"] # Composite destination score (lower is better) dest_score = ( 0.50 * normalize_value(transport_seconds, 1800) # transport time + 0.35 * capacity_ratio # ED crowding + 0.15 * (0.0 if len(hospital["capabilities"]) > 3 else 0.3) # slight preference for higher-capability hospitals ) eligible.append({ **hospital, "transport_time_seconds": round(transport_seconds), "transport_time_minutes": round(transport_seconds / 60, 1), "capacity_ratio": round(capacity_ratio, 2), "score": round(dest_score, 4), }) if not eligible: logger.error("No eligible hospitals found for patient needs: %s", patient_needs) return None # Sort by score ascending (best destination first) eligible.sort(key=lambda h: h["score"]) return eligible[0]
Step 5: Coverage Optimization (Repositioning)
The pseudocode calls this optimize_repositioning. This is the background solver that runs every few minutes. It identifies coverage gaps (zones where no available unit can respond within the target time) and computes optimal unit repositioning moves to close those gaps. This is where OR-Tools earns its keep.
def build_demand_zones(): """ Define coverage zones with centroids and demand weights. In production, these come from the demand forecast model (SageMaker endpoint) which predicts call probability per zone for the next 30 minutes. """ zones = { "ZONE-A": {"centroid": (38.9072, -77.0369), "demand_weight": 0.30}, "ZONE-B": {"centroid": (38.9200, -77.0200), "demand_weight": 0.25}, "ZONE-C": {"centroid": (38.8950, -77.0500), "demand_weight": 0.20}, "ZONE-D": {"centroid": (38.9300, -77.0100), "demand_weight": 0.15}, "ZONE-E": {"centroid": (38.8800, -77.0300), "demand_weight": 0.10}, } return zones def identify_coverage_gaps(fleet, demand_zones): """ Find zones where no available unit can respond within the coverage threshold. Returns a list of gap dicts: zone_id, centroid, best_current_time, demand_weight. These are the zones the repositioning optimizer needs to fix. """ available_units = [u for u in fleet if u["status"] == "AVAILABLE"] gaps = [] for zone_id, zone_info in demand_zones.items(): # Skip very low demand zones (not worth repositioning for) if zone_info["demand_weight"] < 0.05: continue centroid = zone_info["centroid"] # Find the fastest available unit that could reach this zone best_time = float("inf") for unit in available_units: travel = estimate_travel_time_seconds( unit["latitude"], unit["longitude"], centroid[0], centroid[1], emergency=True, ) best_time = min(best_time, travel) if best_time > COVERAGE_THRESHOLD_SECONDS: gaps.append({ "zone_id": zone_id, "centroid": centroid, "best_current_time": round(best_time), "demand_weight": zone_info["demand_weight"], }) return gaps def optimize_repositioning(fleet, demand_zones): """ Solve the repositioning problem: which idle units should move where to close coverage gaps? This is formulated as a weighted set-covering assignment problem: - Decision variables: binary (assign unit i to gap j, yes/no) - Objective: minimize total repositioning cost weighted by coverage priority - Constraints: each gap covered by at most one unit, total moves <= MAX Uses OR-Tools' linear solver (SCIP backend) which handles this scale (5-50 units, 5-200 zones) in milliseconds. """ gaps = identify_coverage_gaps(fleet, demand_zones) if not gaps: logger.info("Coverage is adequate. No repositioning needed.") return [] available_units = [u for u in fleet if u["status"] == "AVAILABLE"] if not available_units: logger.warning("No available units for repositioning.") return [] logger.info("Found %d coverage gaps. %d units available for repositioning.", len(gaps), len(available_units)) # Create the solver solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: logger.error("Could not create SCIP solver. Is OR-Tools installed correctly?") return [] # Decision variables: x[i][j] = 1 if unit i is assigned to cover gap j x = {} for i, unit in enumerate(available_units): for j, gap in enumerate(gaps): x[i, j] = solver.IntVar(0, 1, f"x_{i}_{j}") # Constraint: each unit can be assigned to at most one gap for i in range(len(available_units)): solver.Add( sum(x[i, j] for j in range(len(gaps))) <= 1 ) # Constraint: each gap gets at most one unit assigned for j in range(len(gaps)): solver.Add( sum(x[i, j] for i in range(len(available_units))) <= 1 ) # Constraint: total moves limited solver.Add( sum(x[i, j] for i in range(len(available_units)) for j in range(len(gaps))) <= MAX_REPOSITION_MOVES ) # Objective: minimize repositioning cost, weighted by gap priority. # Cost = travel time to reach the gap's centroid. # Priority = demand weight of the uncovered zone (higher demand = more important to cover). objective = solver.Objective() for i, unit in enumerate(available_units): for j, gap in enumerate(gaps): travel = estimate_travel_time_seconds( unit["latitude"], unit["longitude"], gap["centroid"][0], gap["centroid"][1], emergency=False, # repositioning is non-emergency driving ) # Cost coefficient: travel time penalized, but reduced for high-demand gaps # (we're willing to drive farther to cover a high-demand zone) cost = travel * (1.0 - 0.5 * gap["demand_weight"]) objective.SetCoefficient(x[i, j], cost) objective.SetMinimization() # Solve status = solver.Solve() if status not in (pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE): logger.warning("Repositioning solver did not find a feasible solution.") return [] # Extract solution: which units move where? moves = [] for i, unit in enumerate(available_units): for j, gap in enumerate(gaps): if x[i, j].solution_value() > 0.5: travel = estimate_travel_time_seconds( unit["latitude"], unit["longitude"], gap["centroid"][0], gap["centroid"][1], emergency=False, ) moves.append({ "unit_id": unit["unit_id"], "from_zone": unit["zone"], "to_zone": gap["zone_id"], "target_position": { "lat": gap["centroid"][0], "lng": gap["centroid"][1], }, "repositioning_time_seconds": round(travel), "gap_demand_weight": gap["demand_weight"], "reason": f"Coverage gap in {gap['zone_id']} " f"(best current response: {gap['best_current_time']}s, " f"threshold: {COVERAGE_THRESHOLD_SECONDS}s)", }) logger.info("Repositioning solution: %d moves.", len(moves)) return moves
Step 6: Store Dispatch Decision
Every dispatch decision is written to DynamoDB for the audit trail. In EMS, every decision is reviewable. Medical directors, quality assurance teams, and legal proceedings all need to understand why a particular unit was sent to a particular call.
def store_dispatch_decision(decision): """ Write a dispatch decision record to DynamoDB. This is the audit trail. Every field matters for post-incident review: - Which unit was assigned and why? - What were the alternatives? - What was the estimated response time? - What hospital was recommended? - Did the dispatcher accept or override the recommendation? In production, this table uses an IAM policy that denies UpdateItem and DeleteItem for all principals except a break-glass role. Records are immutable once written. Retention: 10 years minimum (state EMS record retention requirements vary; default to the most stringent). After 12 months, records export to S3 with Object Lock in Compliance mode for tamper-evident long-term archival. """ table = dynamodb.Table(DISPATCH_TABLE_NAME) # DynamoDB requires Decimal for numeric values, not float. # This is a known boto3 gotcha that will raise TypeError if you forget. record = json.loads(json.dumps(decision), parse_float=Decimal) table.put_item(Item=record) logger.info("Stored dispatch decision: %s", decision.get("call_id", "unknown")) return record
Putting It All Together
Here's the full dispatch pipeline assembled into a single callable function. This is what your Lambda handler would invoke when a 911 call arrives from the CAD system.
def dispatch_ambulance(call): """ Run the full dispatch optimization for a single incoming call. Args: call: dict with: - call_id: unique identifier from CAD system - location: {lat, lng} - priority: 1-5 (1 = life-threatening) - required_capability: "ALS" or "BLS" - nature_code: e.g., "CHEST_PAIN", "FALL", "MVA" - patient_age: (optional) for clinical context Returns: Complete dispatch decision dict with assigned unit, recommended hospital, coverage impact assessment, and all candidates considered. """ logger.info("=" * 60) logger.info("DISPATCH REQUEST: %s", call["call_id"]) logger.info(" Priority: %d | Nature: %s | Capability: %s", call["priority"], call["nature_code"], call["required_capability"]) logger.info(" Location: (%.4f, %.4f)", call["location"]["lat"], call["location"]["lng"]) # Load current fleet state (in production: read from DynamoDB) fleet = build_initial_fleet() demand_zones = build_demand_zones() # Score all available, capable units candidates = score_candidates(call, fleet, demand_zones) if not candidates: logger.error("NO AVAILABLE UNITS. Call %s cannot be assigned.", call["call_id"]) return {"call_id": call["call_id"], "status": "UNASSIGNED", "reason": "no_units"} # Best candidate is first in the sorted list assigned = candidates[0] logger.info(" Assigned: %s (travel: %ds, score: %.4f)", assigned["unit_id"], assigned["travel_time_seconds"], assigned["score"]) # Select destination hospital based on call nature # (In reality, hospital selection happens after on-scene assessment, # but we compute a preliminary recommendation at dispatch time) patient_needs = _infer_patient_needs(call) hospitals = build_hospital_status() # Use call location as proxy for unit location (unit hasn't arrived yet) recommended_hospital = select_hospital( patient_needs, call["location"], hospitals ) if recommended_hospital: logger.info(" Hospital: %s (transport: %.1f min, capacity: %d%%)", recommended_hospital["name"], recommended_hospital["transport_time_minutes"], int(recommended_hospital["capacity_ratio"] * 100)) # Build the complete dispatch decision record decision = { "call_id": call["call_id"], "call_priority": call["priority"], "call_location": call["location"], "nature_code": call["nature_code"], "assigned_unit": { "unit_id": assigned["unit_id"], "capability": assigned["capability"], "estimated_response_time_seconds": assigned["travel_time_seconds"], }, "recommended_hospital": { "hospital_id": recommended_hospital["hospital_id"], "name": recommended_hospital["name"], "transport_time_minutes": recommended_hospital["transport_time_minutes"], "capacity_ratio": recommended_hospital["capacity_ratio"], } if recommended_hospital else None, "coverage_impact": { "assigned_unit_impact": assigned["coverage_impact"], "backup_available": assigned["coverage_impact"] < 0.5, }, "candidates_evaluated": len(candidates), "all_candidates": candidates[:5], # top 5 for audit trail "decision_timestamp": datetime.datetime.now(timezone.utc).isoformat(), "target_response_time_seconds": RESPONSE_TIME_TARGETS[call["priority"]], "meets_target": ( assigned["travel_time_seconds"] <= RESPONSE_TIME_TARGETS[call["priority"]] ), # Dispatcher-in-the-loop fields (populated by the dispatcher console # in production; here we simulate auto-accept for Priority 1): "dispatcher_action": "auto_dispatched" if call["priority"] == 1 else "pending", "dispatcher_id": None, # filled by dispatcher console on accept/override "override_reason_code": None, } logger.info(" Meets target: %s (%ds vs %ds threshold)", decision["meets_target"], assigned["travel_time_seconds"], RESPONSE_TIME_TARGETS[call["priority"]]) logger.info("=" * 60) # In production, persist the decision for the audit trail: # store_dispatch_decision(decision) return decision def _infer_patient_needs(call): """ Map call nature codes to hospital capability requirements. In production, this mapping comes from medical direction protocols and is maintained by the EMS medical director. """ nature_to_capabilities = { "CHEST_PAIN": {"required_capabilities": ["cath_lab"], "acuity_level": 1}, "STEMI": {"required_capabilities": ["cath_lab", "interventional_cardiology"], "acuity_level": 1}, "STROKE": {"required_capabilities": ["stroke_center"], "acuity_level": 1}, "TRAUMA_MAJOR": {"required_capabilities": ["level_1_trauma"], "acuity_level": 1}, "FALL": {"required_capabilities": [], "acuity_level": 3}, "MVA": {"required_capabilities": ["general_surgery"], "acuity_level": 2}, "DIFFICULTY_BREATHING": {"required_capabilities": [], "acuity_level": 2}, } return nature_to_capabilities.get( call["nature_code"], {"required_capabilities": [], "acuity_level": 3}, ) # --- Run the full example --- if __name__ == "__main__": # Simulate an incoming Priority 1 cardiac call incoming_call = { "call_id": "CAD-2026-0601-1847", "location": {"lat": 38.9100, "lng": -77.0350}, "priority": 1, "required_capability": "ALS", "nature_code": "CHEST_PAIN", "patient_age": 67, } # Run dispatch optimization decision = dispatch_ambulance(incoming_call) print("\n--- DISPATCH DECISION ---") print(json.dumps(decision, indent=2, default=str)) # Run background repositioning optimization print("\n--- REPOSITIONING OPTIMIZATION ---") fleet = build_initial_fleet() demand_zones = build_demand_zones() moves = optimize_repositioning(fleet, demand_zones) for move in moves: print(f" MOVE: {move['unit_id']} -> {move['to_zone']} " f"({move['repositioning_time_seconds']}s) | {move['reason']}") if not moves: print(" No repositioning needed. Coverage is adequate.")
The Gap Between This and Production
This example works. Run it and you'll get a scored dispatch decision and repositioning recommendations. But there's a meaningful distance between "works in a script" and "handles real 911 calls." Here's where that gap lives:
Real travel times. The Haversine approximation is wildly inaccurate for urban routing. A point 1 km away across a river with no bridge might be 15 minutes by road. Production requires Amazon Location Service (or equivalent) with real-time traffic data. You'd call calculate_route_matrix to get many-to-many travel times for all candidate units simultaneously, then cache results in ElastiCache with a 2-3 minute TTL.
GPS streaming pipeline. This example uses static fleet positions. Production ingests GPS updates every 5-15 seconds from every unit via Kinesis Data Streams. A Lambda consumer updates DynamoDB on each fix. You need conditional writes (only accept newer timestamps) to handle out-of-order delivery, and you need to detect GPS staleness (unit hasn't reported in 60 seconds = position is unreliable).
Demand forecasting. The static demand weights here are a placeholder. Production trains a time-series model on historical call data (time of day, day of week, weather, events) and hosts it on a SageMaker endpoint. The repositioning optimizer queries this model every cycle to get current demand predictions. Even a simple model (gradient-boosted trees on temporal features) dramatically outperforms static weights.
Hospital status integration. Hospital diversion status, ED census, and bed availability change constantly. Production subscribes to hospital system feeds (HL7 ADT messages, proprietary APIs, or even manual updates from hospital liaisons) via EventBridge. The hospital status table in DynamoDB must have a "last updated" timestamp so you can detect stale data and fall back to conservative assumptions.
CAD system integration. The dispatch decision needs to flow back to the Computer-Aided Dispatch system that dispatchers use. This is typically a proprietary system (Tyler New World, Hexagon, Motorola PremierOne) with its own API or HL7 interface. Integration testing with the CAD vendor is a multi-month effort.
Latency budget. A Priority 1 dispatch must complete in under 2 seconds end-to-end. That means: API Gateway receives the request (50ms), Lambda cold start (eliminated with provisioned concurrency), DynamoDB reads (single-digit ms), travel time lookups (cache hit: 1ms, cache miss with Location Service: 200-500ms), scoring computation (10-50ms), DynamoDB write (single-digit ms). Every millisecond matters. Profile relentlessly.
Error handling and fallback. If the Location Service is down, fall back to cached travel times (even if stale). If DynamoDB is unreachable, fall back to an in-memory snapshot. If the scoring function throws an exception, fall back to pure proximity dispatch. The system must always produce an answer. "Sorry, the optimizer crashed" is not acceptable when someone is having a heart attack.
Simulation and backtesting. Before deploying any change to the scoring weights or coverage model, replay 6 months of historical calls through the new logic and compare response time distributions against the old logic. Build a discrete-event simulator that models unit state transitions, travel times, on-scene times, and hospital turnaround times. This is your safety net.
Dispatcher override tracking. Dispatchers will (and should) override the system's recommendations. Track every override: which unit was recommended, which was actually sent, and why. This data is gold for improving the model. If dispatchers consistently override in a specific scenario, the model is missing something.
Multi-casualty incident (MCI) mode. The optimization model assumes independent, sequential calls. An MCI (bus crash, building collapse) generates 5-20 simultaneous patients and requires a completely different allocation strategy. Production needs an MCI detection trigger that switches the system from normal optimization to MCI protocols (triage-based allocation, staging areas, transport coordination).
Audit and compliance. Every dispatch decision is a medical record. Retain for 7+ years per state EMS regulations. Encrypt at rest with KMS CMK. Enable CloudTrail for all API calls. The DynamoDB table needs point-in-time recovery enabled. Consider DynamoDB export to S3 for long-term archival.
Part of the Healthcare AI/ML Cookbook. See Recipe 14.8 for the full architectural walkthrough, pseudocode, and honest take on where this gets hard.