AI Emergency Control Centre

Build a local simulator for synthetic emergency triage and ambulance routing.

Introduction

30 Second Summary

An ambulance can reach the closest hospital quickly yet arrive somewhere without the right specialist or a free critical-care bed. A regional coordinator needs one clear view of the whole network before approving a destination.

In this project, you will build a local emergency control-centre simulator that turns fictional cases into explainable priorities plus capacity-aware hospital recommendations. Human-in-the-loop control keeps every route subject to your approval.

What You'll Build

Your browser will become a regional command view that carries one selected emergency from triage through a mapped hospital route.

By the end of this project, you'll have:

  • A live network dashboard that lets you monitor recent cases against ambulance availability plus current ED and ICU capacity.
  • An explainable decision-tree prediction that shows a synthetic priority with its confidence score plus influential model factors.
  • A human-controlled dispatch workflow where you can compare destinations, approve or override a route, dispatch an ambulance, and review the audit log.
  • Secret Mission: Simulate a network-wide ICU shortage and document the control centre's escalation response.

Are there any prerequisites?

No prior Python or emergency-operations experience is required. You only need a Windows computer with Visual Studio Code, a web browser, and internet access because the guide handles the full software setup.

Before We Start

Before any hands-on work, commit to building an educational emergency control-centre simulator for students or controllers using synthetic cases, recommendations, capacity values, and model outputs. Human approval remains required because the simulator must never guide patient care or autonomous emergency dispatch.

Set Up the Windows Project

Your simulator needs a local runtime before its dashboard can appear. Visual Studio Code is already installed on your computer.

The missing layer is Python 3.14.8. The pinned Streamlit, scikit-learn, and PyDeck packages provide the foundation for your first visible local app.

In this step, get ready to:
  • Confirm the Python 3.14.8 runtime in the VS Code terminal.
  • Install the three pinned packages from requirements.txt.
  • Launch app.py as a local Streamlit page.
Install and verify Python

The py launcher lets Windows find your installed Python runtime. Checking its version first reveals whether you need a fresh installation.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open VS Code.
  • Select View from the top menu.
  • Select Terminal to open the integrated terminal.

Before you run the check, predict which Python version Windows will report:

  • Check the available Python runtime by running this command:
py --version

What does this command do?

The command asks the Windows Python launcher to report its current default runtime. The result determines which setup path you need.

✔️ I see version 3.14.8

Your terminal should show Python 3.14.8. That is the exact runtime this project uses.

ⓧ I see an older version

An older runtime can cause package compatibility problems. Install the pinned release before continuing.

  • Return to the official Python Windows downloads page.
  • Locate the Python 3.14.8 release.
  • Select Download Windows installer (64-bit).
  • Run the downloaded installer.
  • Approve the Windows permission request if it appears.
  • Complete the installer prompts.

A fresh terminal picks up the new runtime after installation. The existing terminal can still hold the earlier environment.

  • Select Terminal from the VS Code menu.
  • Select New Terminal.
  • Confirm the new Python runtime by running this command:
py --version

What should I see?

The fresh terminal should report Python 3.14.8. Windows can now use the required runtime.

Still seeing the older version?

Close VS Code after the installation finishes. Reopen it through Windows search before repeating the version check.

If the older runtime remains active, use the installer again to confirm that Python 3.14.8 completed successfully.

Ask for help with the active runtime: Why does the VS Code terminal still report my older Python version after installing Python 3.14.8 on Windows?

ⓧ Command not found

Windows cannot find a Python runtime yet. The official Windows installer adds the runtime needed by this project.

  • Visit the official Python Windows downloads page.
  • Locate the Python 3.14.8 release.
  • Select Download Windows installer (64-bit).
  • Run the downloaded installer.
  • Approve the Windows permission request if it appears.
  • Complete the installer prompts.

The runtime is now installed. A fresh terminal lets Windows discover the new launcher.

  • Select Terminal from the VS Code menu.
  • Select New Terminal.
  • Confirm the installation by running this command:
py --version

What should I see?

The terminal should report Python 3.14.8. The launcher is ready for your project commands.

Is the command still missing?

Close VS Code after the installer finishes. Reopen it through Windows search before repeating the check.

Run the Python installer again if the installation did not reach its completion screen.

Ask for help with the launcher: Why is the py command unavailable in a fresh VS Code terminal after installing Python 3.14.8 on Windows?

Prepare the project workspace

A VS Code workspace keeps your files in one named folder. Its integrated terminal starts from that folder automatically.

  • Select File from the VS Code menu.
  • Select Open Folder....
  • Choose your Desktop in the folder dialog.
  • Select New Folder.
  • Enter emergency-control-centre as the folder name.
  • Select the emergency-control-centre folder.
  • Select Select Folder.
  • Confirm that you trust the folder if the Workspace Trust dialog appears.

The Explorer sidebar now shows emergency-control-centre as your open workspace. New terminals start inside this folder.

  • Select New File... in the Explorer sidebar.
  • Enter requirements.txt as the file name.
  • Press Enter to create the file.
  • Add the pinned project packages by entering this code:
streamlit==1.65.0
scikit-learn==1.9.1
pydeck==0.9.3

What does this file control?

  • The streamlit==1.65.0 line pins the dashboard framework.
  • The scikit-learn==1.9.1 line pins the machine-learning library used later in the simulator.
  • The pydeck==0.9.3 line pins the mapping library used later in the simulator.
  • Press Ctrl+S to save requirements.txt.

The first package installation can take several minutes. Continuing terminal output means the process is still active.

  • Select View from the VS Code menu.
  • Select Terminal.
  • Install the pinned packages by running this command:
py -m pip install -r requirements.txt

What does this command do?

The py launcher runs pip with your confirmed Python runtime. Pip reads each exact package pin from requirements.txt before installing it.

Good progress. The terminal returns to a PowerShell prompt after processing all three packages without an error.

Did the package installation fail?

  • Confirm that your internet connection is active because the packages must be downloaded.
  • Check that the Explorer sidebar shows requirements.txt inside emergency-control-centre.
  • Confirm that the terminal prompt points to the emergency-control-centre folder.

Ask for help with the installation: Why did pip fail to install my pinned Streamlit, scikit-learn, and PyDeck packages on Python 3.14.8?

✔️ Awesome, I've got everything!

Your saved requirements.txt now holds all three package pins.

ⓧ I'd like to double check the full code

streamlit==1.65.0
scikit-learn==1.9.1
pydeck==0.9.3

This reference contains the exact package pins used by the installation command.

Launch the starter dashboard

A minimal Streamlit page tests the complete path from Python code to a browser. This visible result confirms that your runtime and dashboard package work together.

  • Select New File... in the Explorer sidebar.
  • Enter app.py as the file name.
  • Press Enter to create the file.
  • Add the starter interface by entering this code:
import streamlit as st

st.write("# Emergency Control Centre Simulator")

What does this code do?

  • The import streamlit as st line makes Streamlit available through the shorter st name.
  • The st.write() call renders the simulator title as a page heading.
  • Press Ctrl+S to save app.py.

Running the app starts a local server. The terminal remains occupied until you press Ctrl+C to stop it.

Before you run the app, predict what the browser will show from these two lines:

  • Start the local Streamlit app by running this command:
streamlit run app.py

What does this command do?

Streamlit starts a local server from app.py. It opens the app in a new tab in your default browser.

Your browser should show Emergency Control Centre Simulator as the page heading. That first screen proves your Windows project can run from code to browser.

Did the starter page fail to open?

  • Confirm that app.py appears inside emergency-control-centre in the Explorer sidebar.
  • Check that the terminal started from the emergency-control-centre folder.
  • Open the local address printed in the terminal if the browser did not open automatically.

Ask for help with the launch: Why does my Streamlit starter app fail to open from the VS Code terminal on Windows?

✔️ Awesome, I've got everything!

Your saved app.py matches the complete starter app for this step.

ⓧ I'd like to double check the full code

import streamlit as st

st.write("# Emergency Control Centre Simulator")

This reference is the complete starter app for this step.

Your Windows environment is ready to run the simulator. Next, you will turn this starter page into a shared view of the regional emergency network.

Build the Regional Network Dashboard

Your starter Streamlit page is already running from Visual Studio Code. That confirms the local app can reach your browser.

A control centre needs one shared operational picture before it can make a routing decision. In this step, you will build that picture from fictional regional data.

In this step, get ready to:
  • Define fictional hospitals, ambulances, and recent emergencies.
  • Display live network metrics and operational tables.
  • Render colored network markers on a regional map.
Create the regional data model

Synthetic data gives the simulator realistic structure without representing real patients or facilities. Each record carries the operational fields that the dashboard needs.

  • Select the emergency-control-centre folder in the VS Code Explorer sidebar.
  • Click the New File button at the top of the Explorer sidebar.
  • Type data.py into the filename field.
  • Press Enter.
  • Define the three fictional hospitals by pasting this code into data.py:
def get_hospitals():
    return [
        {"id": "H1", "name": "Central Trauma Centre", "coordinates": [-0.1180, 51.5060], "icu_available": 2, "ed_beds_available": 4, "load_percent": 60, "specialties": ["General", "Trauma", "Cardiac"]},
        {"id": "H2", "name": "Riverside Community Hospital", "coordinates": [-0.1450, 51.5150], "icu_available": 0, "ed_beds_available": 1, "load_percent": 92, "specialties": ["General"]},
        {"id": "H3", "name": "North Regional Hospital", "coordinates": [-0.0950, 51.5350], "icu_available": 1, "ed_beds_available": 3, "load_percent": 40, "specialties": ["General", "Trauma", "Neurology"]},
    ]

What does this code do?

  • The get_hospitals() function returns three fictional facilities.
  • Each hospital includes coordinates for its future map marker.
  • The capacity fields describe ICU beds, ED beds, current load, and available specialties.
  • Save data.py.
  • Expand the Outline section in the Explorer sidebar.

You should see get_hospitals in the file outline. That confirms VS Code recognizes the first data function.

Can't see the hospital function?

  • Confirm the file is named data.py inside emergency-control-centre.
  • Check that def get_hospitals(): begins at the far left of the editor.
  • Match the indentation beneath the function definition exactly.

Help me fix my hospital data function.

  • Add the ambulance records below get_hospitals() by pasting this code:
def get_ambulances():
    return [
        {"id": "A1", "status": "Available", "coordinates": [-0.1320, 51.5100], "case_id": ""},
        {"id": "A2", "status": "Available", "coordinates": [-0.1080, 51.5220], "case_id": ""},
        {"id": "A3", "status": "At hospital", "coordinates": [-0.0950, 51.5350], "case_id": "E-201"},
    ]

How is ambulance state represented?

  • The get_ambulances() function returns three vehicles.
  • The status field identifies whether a vehicle can accept a dispatch.
  • The case_id field connects a busy vehicle to its current emergency.
  • Save data.py.
  • Check the Outline section again.

You should now see get_ambulances beneath get_hospitals. Your data module now tracks the regional vehicle fleet.

Is the ambulance function missing?

  • Leave two blank lines between the hospital function and the ambulance function.
  • Confirm get_ambulances() is aligned with get_hospitals().
  • Check every ambulance dictionary for matching braces.

Help me debug the ambulance records in data.py.

  • Add the emergency records below get_ambulances() by pasting this code:
def get_emergencies():
    return [
        {"id": "E-204", "reported": "2 minutes ago", "summary": "Major road collision", "specialty": "Trauma", "coordinates": [-0.1510, 51.5090], "severity_signal": 9, "breathing_risk": 8, "circulation_risk": 7, "consciousness_risk": 8, "travel_minutes": {"H1": 12, "H2": 5, "H3": 15}, "ambulance_eta_minutes": {"A1": 4, "A2": 8, "A3": 11}},
        {"id": "E-203", "reported": "7 minutes ago", "summary": "Possible cardiac event", "specialty": "Cardiac", "coordinates": [-0.1100, 51.4990], "severity_signal": 6, "breathing_risk": 4, "circulation_risk": 6, "consciousness_risk": 5, "travel_minutes": {"H1": 7, "H2": 16, "H3": 14}, "ambulance_eta_minutes": {"A1": 7, "A2": 9, "A3": 15}},
        {"id": "E-202", "reported": "12 minutes ago", "summary": "Minor fall", "specialty": "General", "coordinates": [-0.0860, 51.5260], "severity_signal": 3, "breathing_risk": 2, "circulation_risk": 2, "consciousness_risk": 2, "travel_minutes": {"H1": 13, "H2": 18, "H3": 6}, "ambulance_eta_minutes": {"A1": 13, "A2": 5, "A3": 4}},
    ]

What is stored with each emergency?

  • The get_emergencies() function returns three recent fictional incidents.
  • The risk fields provide inputs for the synthetic triage model built later.
  • The travel dictionaries store simulated times to every hospital and ambulance.
  • Save data.py.
  • Check the Outline section one more time.

You should see all three functions listed. The simulator now has three hospitals, three ambulances, and three recent emergencies to display.

Is the emergency data underlined?

  • Check that every emergency dictionary ends with a closing brace.
  • Confirm each travel_minutes dictionary contains H1, H2, and H3.
  • Confirm each ambulance_eta_minutes dictionary contains A1, A2, and A3.

Help me find the syntax problem in my emergency records.

✔️ Awesome, I've got everything!

Great. Save data.py before connecting it to the dashboard.

ⓧ I'd like to double check the full code

def get_hospitals():
    return [
        {"id": "H1", "name": "Central Trauma Centre", "coordinates": [-0.1180, 51.5060], "icu_available": 2, "ed_beds_available": 4, "load_percent": 60, "specialties": ["General", "Trauma", "Cardiac"]},
        {"id": "H2", "name": "Riverside Community Hospital", "coordinates": [-0.1450, 51.5150], "icu_available": 0, "ed_beds_available": 1, "load_percent": 92, "specialties": ["General"]},
        {"id": "H3", "name": "North Regional Hospital", "coordinates": [-0.0950, 51.5350], "icu_available": 1, "ed_beds_available": 3, "load_percent": 40, "specialties": ["General", "Trauma", "Neurology"]},
    ]


def get_ambulances():
    return [
        {"id": "A1", "status": "Available", "coordinates": [-0.1320, 51.5100], "case_id": ""},
        {"id": "A2", "status": "Available", "coordinates": [-0.1080, 51.5220], "case_id": ""},
        {"id": "A3", "status": "At hospital", "coordinates": [-0.0950, 51.5350], "case_id": "E-201"},
    ]


def get_emergencies():
    return [
        {"id": "E-204", "reported": "2 minutes ago", "summary": "Major road collision", "specialty": "Trauma", "coordinates": [-0.1510, 51.5090], "severity_signal": 9, "breathing_risk": 8, "circulation_risk": 7, "consciousness_risk": 8, "travel_minutes": {"H1": 12, "H2": 5, "H3": 15}, "ambulance_eta_minutes": {"A1": 4, "A2": 8, "A3": 11}},
        {"id": "E-203", "reported": "7 minutes ago", "summary": "Possible cardiac event", "specialty": "Cardiac", "coordinates": [-0.1100, 51.4990], "severity_signal": 6, "breathing_risk": 4, "circulation_risk": 6, "consciousness_risk": 5, "travel_minutes": {"H1": 7, "H2": 16, "H3": 14}, "ambulance_eta_minutes": {"A1": 7, "A2": 9, "A3": 15}},
        {"id": "E-202", "reported": "12 minutes ago", "summary": "Minor fall", "specialty": "General", "coordinates": [-0.0860, 51.5260], "severity_signal": 3, "breathing_risk": 2, "circulation_risk": 2, "consciousness_risk": 2, "travel_minutes": {"H1": 13, "H2": 18, "H3": 6}, "ambulance_eta_minutes": {"A1": 13, "A2": 5, "A3": 4}},
    ]
Display the operational picture

Streamlit reruns the script whenever the app refreshes. Session State keeps the hospital and ambulance lists available across those reruns.

  • Switch back to app.py in the VS Code editor.
  • Replace its starter contents with this code:
import pydeck as pdk
import streamlit as st

from data import get_ambulances, get_emergencies, get_hospitals

if "hospitals" not in st.session_state:
    st.session_state["hospitals"] = get_hospitals()
if "ambulances" not in st.session_state:
    st.session_state["ambulances"] = get_ambulances()

hospitals = st.session_state["hospitals"]
ambulances = st.session_state["ambulances"]
emergencies = get_emergencies()

st.write("# Emergency Control Centre Simulator")

How does the dashboard load its data?

  • The imports connect app.py to PyDeck, Streamlit, and the three data functions.
  • The first two checks initialize hospital and ambulance state once per browser session.
  • The local variables give the rest of the dashboard shorter references to each dataset.
  • Save app.py.
  • Return to the browser tab running the local app.

You should still see Emergency Control Centre Simulator with no Python error displayed. Good progress. Your starter now loads all three regional datasets.

Seeing an import error?

  • Confirm app.py and data.py appear together inside emergency-control-centre.
  • Check that the imported function names match the definitions in data.py.
  • Save both files before refreshing the browser.

Help me fix the data import in app.py.

  • Add the network status section below the title by pasting this code:
st.write("Educational simulation only. All cases, hospitals, coordinates, scores, and model labels are synthetic and must not be used for patient care.")
st.write("## Live network status")
st.metric("Recent emergencies", len(emergencies))
st.metric("Available ambulances", sum(ambulance["status"] == "Available" for ambulance in ambulances))
st.metric("Open ICU beds", sum(hospital["icu_available"] for hospital in hospitals))

What do the status metrics show?

  • The opening notice makes the simulator's educational boundary visible.
  • The emergency metric counts every case returned by get_emergencies().
  • The remaining metrics calculate available vehicles and open ICU beds from current state.
  • Save app.py.
  • Return to the local app in your browser.

You should see three live metrics beneath the educational notice. They show 3 recent emergencies, 2 available ambulances, and 3 open ICU beds.

Do the metrics show different values?

  • Compare the hospital capacity values in data.py with the supplied records.
  • Confirm only A1 and A2 have the Available status.
  • Refresh the browser after saving both Python files.

Help me trace the incorrect dashboard metrics.

  • Add the three operational tables below the ICU metric by pasting this code:
hospital_rows = [{"hospital": hospital["name"], "icu_available": hospital["icu_available"], "ed_beds_available": hospital["ed_beds_available"], "load_percent": hospital["load_percent"], "specialties": ", ".join(hospital["specialties"])} for hospital in hospitals]
st.write("## Hospitals")
st.dataframe(hospital_rows, width="stretch", hide_index=True)
st.write("## Ambulances")
st.dataframe(ambulances, width="stretch", hide_index=True)
st.write("## Recent emergencies")
emergency_rows = [{"case": case["id"], "reported": case["reported"], "summary": case["summary"], "required_specialty": case["specialty"]} for case in emergencies]
st.dataframe(emergency_rows, width="stretch", hide_index=True)

How are the operational tables prepared?

  • The hospital rows keep the capacity, load, and specialty fields needed for routing.
  • The ambulance table displays each vehicle's current status and assigned case.
  • The emergency rows turn each fictional incident into a compact queue entry.
  • Save app.py.
  • Return to the browser tab running the dashboard.

You should see three hospitals, three ambulances, and three recent emergencies. Each hospital row also shows ICU availability, ED availability, load, and specialties.

Is a table missing data?

  • Confirm the table code sits below the three metric calls in app.py.
  • Check that every field name matches its corresponding key in data.py.
  • Look for an error in the browser that names the missing dictionary key.

Help me debug a missing Streamlit table or column.

Plot the regional network map

PyDeck turns the saved coordinates into interactive map layers. A scatterplot layer gives each resource type its own marker color and radius.

  • Add the regional map below the recent emergencies table by pasting this code:
points = []
for hospital in hospitals:
    points.append({"label": f"Hospital: {hospital['name']} | ICU: {hospital['icu_available']} | ED: {hospital['ed_beds_available']}", "coordinates": hospital["coordinates"], "color": [220, 50, 47, 190], "radius": 260})
for ambulance in ambulances:
    ambulance_color = [38, 139, 210, 200] if ambulance["status"] == "Available" else [108, 113, 196, 200]
    points.append({"label": f"Ambulance {ambulance['id']}: {ambulance['status']}", "coordinates": ambulance["coordinates"], "color": ambulance_color, "radius": 180})
for case in emergencies:
    points.append({"label": f"Emergency {case['id']}: {case['summary']}", "coordinates": case["coordinates"], "color": [255, 140, 0, 220], "radius": 220})

network_map = pdk.Deck(layers=[pdk.Layer("ScatterplotLayer", points, pickable=True, get_position="coordinates", get_fill_color="color", get_radius="radius")], initial_view_state=pdk.ViewState(latitude=51.5160, longitude=-0.1190, zoom=11, bearing=0, pitch=0), map_style=None, tooltip={"text": "{label}"})
st.pydeck_chart(network_map, width="stretch", height=420)

How does the map distinguish resources?

  • The three loops convert hospitals, ambulances, and emergencies into map points.
  • Hospitals use red markers while emergencies use orange markers.
  • Ambulance colors change according to current availability.
  • The tooltip reveals the label attached to each point.
  • Save app.py.
  • Return to the VS Code terminal from earlier.
  • Press Ctrl+C if the current app process is still running.

Before you restart the dashboard, what marker groups do you expect the regional map to contain?

  • Restart the local dashboard by running this command:
streamlit run app.py

What does this command do?

The command starts app.py as a local Streamlit application. The running process serves the dashboard to your browser.

  • Switch back to the browser tab for the local Streamlit app.
  • Refresh the page if the updated dashboard is not already visible.
  • Hover over one hospital marker.
  • Hover over one ambulance marker.
  • Hover over one emergency marker.

You should see three hospital markers, three ambulance markers, and three emergency markers. Each hover tooltip should identify the selected resource.

That's the shared operational picture working. The browser now combines live counts, capacity tables, fleet status, recent cases, and regional positions on one dashboard.

The Map Needs Internet Access

PyDeck needs an internet connection to render its map tiles. The local tables and metrics can still run from the data stored in your project.

Map missing or markers invisible?

  • Confirm your computer has an active internet connection for the map tiles.
  • Check that import pydeck as pdk appears at the top of app.py.
  • Compare every coordinate with the corresponding value in data.py.

Help me troubleshoot the regional PyDeck map.

✔️ Awesome, I've got everything!

Excellent. Save app.py and keep the local dashboard running.

ⓧ I'd like to double check the full code

import pydeck as pdk
import streamlit as st

from data import get_ambulances, get_emergencies, get_hospitals

if "hospitals" not in st.session_state:
    st.session_state["hospitals"] = get_hospitals()
if "ambulances" not in st.session_state:
    st.session_state["ambulances"] = get_ambulances()

hospitals = st.session_state["hospitals"]
ambulances = st.session_state["ambulances"]
emergencies = get_emergencies()

st.write("# Emergency Control Centre Simulator")
st.write("Educational simulation only. All cases, hospitals, coordinates, scores, and model labels are synthetic and must not be used for patient care.")
st.write("## Live network status")
st.metric("Recent emergencies", len(emergencies))
st.metric("Available ambulances", sum(ambulance["status"] == "Available" for ambulance in ambulances))
st.metric("Open ICU beds", sum(hospital["icu_available"] for hospital in hospitals))

hospital_rows = [{"hospital": hospital["name"], "icu_available": hospital["icu_available"], "ed_beds_available": hospital["ed_beds_available"], "load_percent": hospital["load_percent"], "specialties": ", ".join(hospital["specialties"])} for hospital in hospitals]
st.write("## Hospitals")
st.dataframe(hospital_rows, width="stretch", hide_index=True)
st.write("## Ambulances")
st.dataframe(ambulances, width="stretch", hide_index=True)
st.write("## Recent emergencies")
emergency_rows = [{"case": case["id"], "reported": case["reported"], "summary": case["summary"], "required_specialty": case["specialty"]} for case in emergencies]
st.dataframe(emergency_rows, width="stretch", hide_index=True)

points = []
for hospital in hospitals:
    points.append({"label": f"Hospital: {hospital['name']} | ICU: {hospital['icu_available']} | ED: {hospital['ed_beds_available']}", "coordinates": hospital["coordinates"], "color": [220, 50, 47, 190], "radius": 260})
for ambulance in ambulances:
    ambulance_color = [38, 139, 210, 200] if ambulance["status"] == "Available" else [108, 113, 196, 200]
    points.append({"label": f"Ambulance {ambulance['id']}: {ambulance['status']}", "coordinates": ambulance["coordinates"], "color": ambulance_color, "radius": 180})
for case in emergencies:
    points.append({"label": f"Emergency {case['id']}: {case['summary']}", "coordinates": case["coordinates"], "color": [255, 140, 0, 220], "radius": 220})

network_map = pdk.Deck(layers=[pdk.Layer("ScatterplotLayer", points, pickable=True, get_position="coordinates", get_fill_color="color", get_radius="radius")], initial_view_state=pdk.ViewState(latitude=51.5160, longitude=-0.1190, zoom=11, bearing=0, pitch=0), map_style=None, tooltip={"text": "{label}"})
st.pydeck_chart(network_map, width="stretch", height=420)

Your regional network is now visible in one browser dashboard. Next, you will test whether choosing the nearest hospital produces a safe result.

Expose Nearest-Hospital Routing Failure

Your regional Streamlit dashboard now presents one shared operational view. You can see the whole fictional emergency network in one place.

A control centre can make a dangerous choice when travel time becomes the only routing signal. You will run that narrow rule on the default trauma case and inspect the result yourself.

In this step, get ready to:
  • Create a nearest-only routing rule that uses simulated travel time.
  • Add an emergency selector with the nearest destination result.
  • Test the default trauma case against hospital capability and ICU capacity.
Create the nearest-only routing rule

The first routing rule has one job. It returns the hospital with the lowest simulated travel time for the selected case.

  • In the Explorer sidebar in Visual Studio Code, click the New File button.
  • Enter routing.py as the file name.
  • Add the nearest-only rule by pasting this code into routing.py:
def nearest_hospital(case, hospitals):
    return min(hospitals, key=lambda hospital: case["travel_minutes"][hospital["id"]])

What does this code do?

  • The nearest_hospital() function receives the selected case and the current hospital list.
  • The min() function compares each hospital using its simulated travel time.
  • The returned hospital is the fastest destination according to this single signal.
  • Save routing.py.
  • Confirm the Explorer sidebar shows routing.py next to app.py.

Good progress. Your project now has a dedicated routing rule that the dashboard can call.

Cannot find the routing file?

  • Check that routing.py sits inside the emergency-control-centre folder.
  • Check that the file name ends with .py.
  • Compare the function name with nearest_hospital.

Ask for help with checking the routing file location and function.

✔️ Awesome, I've got everything!

Your nearest-only routing function is saved in routing.py.

ⓧ I'd like to double check the full code

def nearest_hospital(case, hospitals):
    return min(hospitals, key=lambda hospital: case["travel_minutes"][hospital["id"]])

This reference shows the complete routing.py file for this step.

Connect routing to the dashboard

The dashboard needs access to the new function before it can compare travel times. It also needs a selected emergency to pass into that function.

  • Switch back to app.py in Visual Studio Code.
  • Find this existing import line:
from data import get_ambulances, get_emergencies, get_hospitals

Why use this line as the anchor?

This import marks the part of app.py where project modules become available. The routing import belongs directly below it.

  • Add the routing import directly below the existing data import:
from data import get_ambulances, get_emergencies, get_hospitals
from routing import nearest_hospital

What changed?

The new import makes nearest_hospital() available inside the dashboard. The data import remains unchanged.

  • Find the line that begins with st.dataframe(emergency_rows.
  • Add the emergency selector and nearest-only result below it by pasting this code:
case_label = st.selectbox("Select a recent emergency", [f"{case['id']} | {case['reported']} | {case['summary']}" for case in emergencies])
selected_case_id = case_label.split(" | ")[0]
selected_case = next(case for case in emergencies if case["id"] == selected_case_id)
nearest = nearest_hospital(selected_case, hospitals)

st.write("## Routing comparison")
st.write(f"Nearest-only result: {nearest['name']} at {selected_case['travel_minutes'][nearest['id']]} simulated minutes.")

How does the selection reach the routing rule?

  • The case_label value contains the case ID with its reported time and summary.
  • The selected_case lookup retrieves the full emergency record from the fictional data.
  • The nearest value holds the hospital returned by the travel-time comparison.
  • The routing result displays the chosen hospital with its simulated journey time.
  • Save app.py.

Before you check the dashboard, which destination do you think a travel-time-only rule will choose for E-204?

  • Switch back to the browser tab running the dashboard.
  • Refresh the browser tab.

You will see Riverside Community Hospital selected as the nearest-only destination at 5 simulated minutes.

Do not see the routing comparison?

  • Check that you saved both routing.py and app.py.
  • Check that the routing import matches the nearest_hospital function name.
  • Check that the selector code sits below the recent emergencies table.

Ask for help with debugging the missing routing comparison.

Inspect why the nearest hospital is unsuitable

The five-minute result proves the rule can minimize travel time. The hospital table now provides the operational evidence needed to challenge that choice.

Before you compare the hospital row, do you expect the fastest destination to satisfy both requirements of the trauma scenario?

  • Select E-204 | 2 minutes ago | Major road collision in the Select a recent emergency field.
  • Find Riverside Community Hospital in the hospital table.
  • Read the hospital's specialties value.
  • Read the hospital's icu_available value.

Riverside lists only General capability. Its icu_available value is 0.

The intended routing failure

The nearest-only rule chose Riverside because its simulated travel time is the lowest. The rule never examined the required Trauma capability.

It also ignored ICU availability and hospital load. The shortest route has produced an unsuitable operational destination.

The dashboard should make this shortfall explicit whenever the default trauma scenario produces the Riverside result.

  • Switch back to app.py.
  • Find the line that displays the nearest-only result.
  • Add the suitability warnings directly below that line by pasting this code:
if selected_case["specialty"] not in nearest["specialties"]:
    st.write(f"Nearest-only failure: No {selected_case['specialty']} capability.")
if selected_case["id"] == "E-204" and nearest["icu_available"] == 0:
    st.write("Nearest-only failure: Zero ICU beds for the immediate trauma scenario.")

What do the warnings check?

  • The first condition compares the case's required specialty with the capabilities listed for the nearest hospital.
  • The second condition checks the ICU shortfall in the default immediate trauma scenario.
  • These warnings expose the limitation without changing the nearest-only decision.
  • Save app.py.

Before you perform the final check, what two warnings do you expect beneath the five-minute routing result?

  • Refresh the browser tab running the dashboard.
  • Select E-204 | 2 minutes ago | Major road collision in the emergency selector.

You will see Riverside Community Hospital as the five-minute nearest option. You will also see warnings for its missing Trauma capability and zero ICU beds.

You have caught the exact weakness in the first routing rule. The dashboard now exposes a fast destination that cannot meet the case requirements.

Warnings missing for E-204?

  • Check that E-204 is selected in the emergency selector.
  • Check that the warning conditions sit immediately below the nearest-only result.
  • Check that the comparisons use specialty, specialties, and icu_available with matching spelling.

Ask for help with debugging the E-204 suitability warnings.

✔️ Awesome, I've got everything!

Your dashboard now selects an emergency and exposes the failure of travel-time-only routing.

ⓧ I'd like to double check the full code

import pydeck as pdk
import streamlit as st

from data import get_ambulances, get_emergencies, get_hospitals
from routing import nearest_hospital

if "hospitals" not in st.session_state:
    st.session_state["hospitals"] = get_hospitals()
if "ambulances" not in st.session_state:
    st.session_state["ambulances"] = get_ambulances()

hospitals = st.session_state["hospitals"]
ambulances = st.session_state["ambulances"]
emergencies = get_emergencies()

st.write("# Emergency Control Centre Simulator")
st.write("Educational simulation only. All cases, hospitals, coordinates, scores, and model labels are synthetic and must not be used for patient care.")
st.write("## Live network status")
st.metric("Recent emergencies", len(emergencies))
st.metric("Available ambulances", sum(ambulance["status"] == "Available" for ambulance in ambulances))
st.metric("Open ICU beds", sum(hospital["icu_available"] for hospital in hospitals))

hospital_rows = [{"hospital": hospital["name"], "icu_available": hospital["icu_available"], "ed_beds_available": hospital["ed_beds_available"], "load_percent": hospital["load_percent"], "specialties": ", ".join(hospital["specialties"])} for hospital in hospitals]
st.write("## Hospitals")
st.dataframe(hospital_rows, width="stretch", hide_index=True)
st.write("## Ambulances")
st.dataframe(ambulances, width="stretch", hide_index=True)
st.write("## Recent emergencies")
emergency_rows = [{"case": case["id"], "reported": case["reported"], "summary": case["summary"], "required_specialty": case["specialty"]} for case in emergencies]
st.dataframe(emergency_rows, width="stretch", hide_index=True)

case_label = st.selectbox("Select a recent emergency", [f"{case['id']} | {case['reported']} | {case['summary']}" for case in emergencies])
selected_case_id = case_label.split(" | ")[0]
selected_case = next(case for case in emergencies if case["id"] == selected_case_id)
nearest = nearest_hospital(selected_case, hospitals)

st.write("## Routing comparison")
st.write(f"Nearest-only result: {nearest['name']} at {selected_case['travel_minutes'][nearest['id']]} simulated minutes.")
if selected_case["specialty"] not in nearest["specialties"]:
    st.write(f"Nearest-only failure: No {selected_case['specialty']} capability.")
if selected_case["id"] == "E-204" and nearest["icu_available"] == 0:
    st.write("Nearest-only failure: Zero ICU beds for the immediate trauma scenario.")

points = []
for hospital in hospitals:
    points.append({"label": f"Hospital: {hospital['name']} | ICU: {hospital['icu_available']} | ED: {hospital['ed_beds_available']}", "coordinates": hospital["coordinates"], "color": [220, 50, 47, 190], "radius": 260})
for ambulance in ambulances:
    ambulance_color = [38, 139, 210, 200] if ambulance["status"] == "Available" else [108, 113, 196, 200]
    points.append({"label": f"Ambulance {ambulance['id']}: {ambulance['status']}", "coordinates": ambulance["coordinates"], "color": ambulance_color, "radius": 180})
for case in emergencies:
    points.append({"label": f"Emergency {case['id']}: {case['summary']}", "coordinates": case["coordinates"], "color": [255, 140, 0, 220], "radius": 220})

network_map = pdk.Deck(layers=[pdk.Layer("ScatterplotLayer", points, pickable=True, get_position="coordinates", get_fill_color="color", get_radius="radius")], initial_view_state=pdk.ViewState(latitude=51.5160, longitude=-0.1190, zoom=11, bearing=0, pitch=0), map_style=None, tooltip={"text": "{label}"})
st.pydeck_chart(network_map, width="stretch", height=420)

This reference preserves the regional dashboard and adds only the routing work from this step.

Your simulator now demonstrates why proximity alone cannot control emergency routing. Next, you will add explainable synthetic triage and operational constraints that can recommend an eligible destination.

Add Explainable Triage and Dynamic Routing

Your nearest-only comparison exposed a serious routing gap. Riverside Community Hospital is closest to E-204. Its missing trauma capability makes it unsuitable.

This step adds an explainable decision tree for synthetic priority. Deterministic checks then protect the recommendation with capability rules. Capacity and travel time shape the final ranking.

In this step, get ready to:
  • Train a synthetic triage model that returns a priority with confidence and influential factors.
  • Rank hospitals using specialty requirements plus current ED and ICU capacity.
  • Show the safer recommendation with a route line on the regional map.
Train the synthetic triage model

The model learns from a small set of clearly labeled synthetic data. It demonstrates classification without claiming clinical validity.

Why combine a model with routing rules?

The scikit-learn model makes its synthetic priority visible through confidence and feature importance. A controller can inspect the basis of that prediction.

Routing rules enforce the operational constraints directly. This hybrid approach keeps capability and capacity decisions inspectable.

  • Select the New File button in the VS Code Explorer sidebar.
  • Enter triage.py as the file name.
  • Add the synthetic feature definitions and model builder by pasting this first chunk into triage.py:
from sklearn.tree import DecisionTreeClassifier

FEATURE_NAMES = ["severity_signal", "breathing_risk", "circulation_risk", "consciousness_risk"]
TRAINING_INPUTS = [[9, 8, 8, 9], [8, 9, 7, 8], [10, 7, 9, 7], [7, 8, 8, 8], [6, 5, 5, 5], [5, 7, 4, 4], [7, 4, 6, 5], [4, 6, 5, 3], [2, 2, 2, 1], [3, 1, 2, 2], [1, 3, 1, 1], [4, 2, 3, 2]]
TRAINING_LABELS = ["Immediate", "Immediate", "Immediate", "Immediate", "Urgent", "Urgent", "Urgent", "Urgent", "Standard", "Standard", "Standard", "Standard"]


def build_model():
    model = DecisionTreeClassifier(max_depth=3, random_state=42)
    model.fit(TRAINING_INPUTS, TRAINING_LABELS)
    return model

What does this code do?

  • The feature list names the four synthetic risk signals read from each emergency.
  • The training inputs pair sample risk values with the three synthetic priority labels.
  • The model uses a maximum depth of 3 to keep the decision structure small.
  • The fixed random state makes repeated training deterministic for this simulation.
  • Save triage.py.
  • Check the VS Code Problems panel. You should see no Python syntax problem for the model builder.

Seeing a problem in the model builder?

Check that each training row has four values. Confirm that the label list contains one label for every row.

Compare the capitalization of DecisionTreeClassifier with the import at the top of the file.

Ask for help with the model setup.

  • Add the prediction function below build_model() by pasting this chunk:
def predict_priority(case):
    model = build_model()
    sample = [[case[feature] for feature in FEATURE_NAMES]]
    priority = str(model.predict(sample)[0])
    probabilities = model.predict_proba(sample)[0]
    confidence = float(max(probabilities)) * 100
    factors = sorted(zip(FEATURE_NAMES, model.feature_importances_), key=lambda item: item[1], reverse=True)[:2]
    factor_rows = [{"factor": feature.replace("_", " ").title(), "case_value": case[feature], "model_importance": round(float(importance), 2)} for feature, importance in factors]
    return {"priority": priority, "confidence": round(confidence, 1), "factors": factor_rows}

How is the prediction explained?

  • The function converts the selected emergency into the feature order used during training.
  • The predicted class becomes the synthetic priority shown to the controller.
  • The highest class probability becomes a synthetic confidence percentage.
  • The two strongest feature importances become readable factor rows for the dashboard.
  • Save triage.py.
  • Check the VS Code Problems panel again. You should see no Python syntax problem for predict_priority().

✔️ Awesome, I've got everything

Your synthetic model can now train itself and return an explainable priority result.

ⓧ I'd like to double check the full code

from sklearn.tree import DecisionTreeClassifier

FEATURE_NAMES = ["severity_signal", "breathing_risk", "circulation_risk", "consciousness_risk"]
TRAINING_INPUTS = [[9, 8, 8, 9], [8, 9, 7, 8], [10, 7, 9, 7], [7, 8, 8, 8], [6, 5, 5, 5], [5, 7, 4, 4], [7, 4, 6, 5], [4, 6, 5, 3], [2, 2, 2, 1], [3, 1, 2, 2], [1, 3, 1, 1], [4, 2, 3, 2]]
TRAINING_LABELS = ["Immediate", "Immediate", "Immediate", "Immediate", "Urgent", "Urgent", "Urgent", "Urgent", "Standard", "Standard", "Standard", "Standard"]


def build_model():
    model = DecisionTreeClassifier(max_depth=3, random_state=42)
    model.fit(TRAINING_INPUTS, TRAINING_LABELS)
    return model


def predict_priority(case):
    model = build_model()
    sample = [[case[feature] for feature in FEATURE_NAMES]]
    priority = str(model.predict(sample)[0])
    probabilities = model.predict_proba(sample)[0]
    confidence = float(max(probabilities)) * 100
    factors = sorted(zip(FEATURE_NAMES, model.feature_importances_), key=lambda item: item[1], reverse=True)[:2]
    factor_rows = [{"factor": feature.replace("_", " ").title(), "case_value": case[feature], "model_importance": round(float(importance), 2)} for feature, importance in factors]
    return {"priority": priority, "confidence": round(confidence, 1), "factors": factor_rows}

This reference combines the model builder with the prediction and explanation function.

Rank hospitals with operational constraints

A priority prediction does not choose a safe destination by itself. The routing engine must block hospitals that cannot meet the selected case's specialty or capacity needs.

  • Switch back to routing.py from the previous step.
  • Add rank_hospitals() below nearest_hospital() by pasting this chunk:
def rank_hospitals(case, priority, hospitals):
    ranked = []
    for hospital in hospitals:
        eta = case["travel_minutes"][hospital["id"]]
        specialty_ok = case["specialty"] in hospital["specialties"]
        ed_ok = hospital["ed_beds_available"] > 0
        icu_ok = priority != "Immediate" or hospital["icu_available"] > 0
        eligible = specialty_ok and ed_ok and icu_ok
        issues = []
        if not specialty_ok:
            issues.append(f"No {case['specialty']} capability")
        if not ed_ok:
            issues.append("No ED bed")
        if not icu_ok:
            issues.append("No ICU bed for immediate case")
        if eligible:
            load_penalty = hospital["load_percent"] / 10
            capacity_credit = hospital["ed_beds_available"] * 1.5 + hospital["icu_available"] * 2
            route_score = round(eta + load_penalty - capacity_credit, 1)
            reason = "Capability and capacity available"
        else:
            route_score = 999.0
            reason = "; ".join(issues)
        ranked.append({"hospital_id": hospital["id"], "hospital": hospital["name"], "eta_minutes": eta, "eligible": "Yes" if eligible else "No", "icu_available": hospital["icu_available"], "ed_beds_available": hospital["ed_beds_available"], "load_percent": hospital["load_percent"], "route_score": route_score, "reason": reason})
    return sorted(ranked, key=lambda row: (row["route_score"], row["eta_minutes"]))

How does dynamic ranking work?

  • Specialty matching blocks hospitals that cannot provide the required capability.
  • ED availability blocks hospitals with no open emergency bed.
  • ICU availability becomes mandatory when the synthetic priority is Immediate.
  • Eligible hospitals receive a score based on travel time plus load pressure. Available ED and ICU beds reduce that score.
  • Save routing.py.
  • Check the VS Code Problems panel. You should see no syntax problem for rank_hospitals().

Seeing a routing syntax problem?

Check the indentation inside the hospital loop. Each eligibility check belongs inside that loop.

Confirm that issues starts as a new empty list for each hospital.

Ask for help with the ranking function.

  • Add the recommendation and ambulance-ordering helpers below rank_hospitals() by pasting this chunk:
def recommend_hospital(ranked_hospitals):
    eligible = [row for row in ranked_hospitals if row["eligible"] == "Yes"]
    return eligible[0] if eligible else None


def available_ambulances(case, ambulances):
    available = [ambulance for ambulance in ambulances if ambulance["status"] == "Available"]
    return sorted(available, key=lambda ambulance: case["ambulance_eta_minutes"][ambulance["id"]])

What do these helpers return?

The recommendation helper returns the highest-ranked eligible destination. It returns None when every destination is blocked.

The ambulance helper keeps available vehicles only. It sorts those vehicles by their simulated arrival time to the selected case.

  • Save routing.py.
  • Check the VS Code Problems panel. You should see no Python syntax problems in the completed routing module.

✔️ Awesome, my routing file is ready

Your routing engine can now reject unsuitable hospitals and return the strongest eligible destination.

ⓧ I'd like to double check the full code

def nearest_hospital(case, hospitals):
    return min(hospitals, key=lambda hospital: case["travel_minutes"][hospital["id"]])


def rank_hospitals(case, priority, hospitals):
    ranked = []
    for hospital in hospitals:
        eta = case["travel_minutes"][hospital["id"]]
        specialty_ok = case["specialty"] in hospital["specialties"]
        ed_ok = hospital["ed_beds_available"] > 0
        icu_ok = priority != "Immediate" or hospital["icu_available"] > 0
        eligible = specialty_ok and ed_ok and icu_ok
        issues = []
        if not specialty_ok:
            issues.append(f"No {case['specialty']} capability")
        if not ed_ok:
            issues.append("No ED bed")
        if not icu_ok:
            issues.append("No ICU bed for immediate case")
        if eligible:
            load_penalty = hospital["load_percent"] / 10
            capacity_credit = hospital["ed_beds_available"] * 1.5 + hospital["icu_available"] * 2
            route_score = round(eta + load_penalty - capacity_credit, 1)
            reason = "Capability and capacity available"
        else:
            route_score = 999.0
            reason = "; ".join(issues)
        ranked.append({"hospital_id": hospital["id"], "hospital": hospital["name"], "eta_minutes": eta, "eligible": "Yes" if eligible else "No", "icu_available": hospital["icu_available"], "ed_beds_available": hospital["ed_beds_available"], "load_percent": hospital["load_percent"], "route_score": route_score, "reason": reason})
    return sorted(ranked, key=lambda row: (row["route_score"], row["eta_minutes"]))


def recommend_hospital(ranked_hospitals):
    eligible = [row for row in ranked_hospitals if row["eligible"] == "Yes"]
    return eligible[0] if eligible else None


def available_ambulances(case, ambulances):
    available = [ambulance for ambulance in ambulances if ambulance["status"] == "Available"]
    return sorted(available, key=lambda ambulance: case["ambulance_eta_minutes"][ambulance["id"]])

This reference keeps the nearest-only baseline beside the new ranking and recommendation helpers.

Connect the recommendation to the dashboard

The backend can now explain a priority and rank destinations. The Streamlit dashboard needs to calculate those results for the selected emergency.

  • Switch back to app.py.
  • Replace the existing import from routing.py with these routing and triage imports:
from routing import available_ambulances, nearest_hospital, rank_hospitals, recommend_hospital
from triage import predict_priority

What do these imports connect?

The dashboard can now call the synthetic prediction and dynamic routing functions. The ambulance helper also becomes available for the control workflow.

  • Find the line that resolves selected_case from the selected case ID.
  • Replace that line through the current nearest-only warning with this complete selection and comparison section:
selected_case = next(case for case in emergencies if case["id"] == selected_case_id)
triage_result = predict_priority(selected_case)
ranked = rank_hospitals(selected_case, triage_result["priority"], hospitals)
recommended = recommend_hospital(ranked)
nearest = nearest_hospital(selected_case, hospitals)
nearest_row = next(row for row in ranked if row["hospital_id"] == nearest["id"])

st.write("## AI-assisted synthetic triage")
st.metric("Predicted priority", triage_result["priority"])
st.metric("Synthetic model confidence", f"{triage_result['confidence']:.1f}%")
st.write("Most influential model factors for this small synthetic model:")
st.dataframe(triage_result["factors"], width="stretch", hide_index=True)
st.write("## Routing comparison")
st.write(f"Nearest-only result: {nearest['name']} at {selected_case['travel_minutes'][nearest['id']]} simulated minutes.")
if nearest_row["eligible"] == "No":
    st.write(f"Nearest-only failure: {nearest_row['reason']}.")
routing_rows = [{"hospital": row["hospital"], "eta_minutes": row["eta_minutes"], "eligible": row["eligible"], "icu_available": row["icu_available"], "ed_beds_available": row["ed_beds_available"], "load_percent": row["load_percent"], "route_score": "Blocked" if row["eligible"] == "No" else row["route_score"], "reason": row["reason"]} for row in ranked]
st.dataframe(routing_rows, width="stretch", hide_index=True)
if recommended:
    st.write(f"Dynamic recommendation: {recommended['hospital']}.")
else:
    st.write("No eligible hospital is available. Escalate to regional clinical and incident-command leadership before routing.")

What changes on the dashboard?

  • The selected emergency is passed into the synthetic model before routing begins.
  • The nearest hospital remains visible as a baseline comparison.
  • The ranking table labels unsuitable hospitals as Blocked.
  • The recommendation names the eligible hospital with the lowest route score.
  • Save app.py.

Before you run the dashboard, do you expect the five-minute hospital or the capability-matched hospital to lead the ranking?

  • Run the updated dashboard from the VS Code terminal with this command:
streamlit run app.py

What should you see?

Select E-204. You should see an Immediate synthetic priority with a confidence value.

The factor table should contain two rows. Riverside Community Hospital should be blocked while Central Trauma Centre becomes the dynamic recommendation.

Dashboard stopped after the update?

Check that triage.py and routing.py are inside the same emergency-control-centre folder as app.py.

Confirm that every imported function name matches its definition exactly. A spelling difference prevents the dashboard from starting.

Ask for help with the dashboard connection.

A table explains the recommendation. A route line makes the chosen destination visible across the network.

  • Find the existing layers list near the bottom of app.py.
  • Replace that line with this PyDeck layer and route section:
layers = [pdk.Layer("ScatterplotLayer", points, pickable=True, get_position="coordinates", get_fill_color="color", get_radius="radius")]
route_destination = chosen_hospital
if route_destination is None and recommended:
    route_destination = next(hospital for hospital in hospitals if hospital["id"] == recommended["hospital_id"])
if route_destination:
    route_data = [{"start": selected_case["coordinates"], "end": route_destination["coordinates"]}]
    layers.append(pdk.Layer("LineLayer", route_data, get_source_position="start", get_target_position="end", get_color=[0, 120, 255, 220], get_width=8))

How is the route drawn?

The selected emergency coordinates become the route's starting point. The recommended hospital coordinates become its destination.

The LineLayer joins those points with a blue line. The line disappears when no eligible recommendation exists.

  • Save app.py.
  • Select E-204 from the emergency selector.

Before you refresh the dashboard, which hospital do you expect the blue route to reach?

  • Refresh the browser page showing the running dashboard.
  • Hover over the destination marker to identify the hospital at the end of the blue route.

You should see Riverside Community Hospital listed as the five-minute nearest option. Its ranking row should show No under eligibility.

Central Trauma Centre should be the dynamic recommendation. The blue route should run from E-204 to its hospital marker.

Route line missing from the map?

Confirm that E-204 is selected. Check that Central Trauma Centre still has an available ICU bed and an available ED bed.

Verify that the new LineLayer is appended to the same layers list passed into pdk.Deck().

Ask for help with the network route.

✔️ The triage and route are working

Great work. Your control-centre dashboard now explains the synthetic priority and rejects the unsafe nearest destination.

ⓧ I'd like to double check the full code

import pydeck as pdk
import streamlit as st

from data import get_ambulances, get_emergencies, get_hospitals
from routing import available_ambulances, nearest_hospital, rank_hospitals, recommend_hospital
from triage import predict_priority

if "hospitals" not in st.session_state:
    st.session_state["hospitals"] = get_hospitals()
if "ambulances" not in st.session_state:
    st.session_state["ambulances"] = get_ambulances()

hospitals = st.session_state["hospitals"]
ambulances = st.session_state["ambulances"]
emergencies = get_emergencies()

st.write("# Emergency Control Centre Simulator")
st.write("Educational simulation only. All cases, hospitals, coordinates, scores, and model labels are synthetic and must not be used for patient care.")
st.write("## Live network status")
st.metric("Recent emergencies", len(emergencies))
st.metric("Available ambulances", sum(ambulance["status"] == "Available" for ambulance in ambulances))
st.metric("Open ICU beds", sum(hospital["icu_available"] for hospital in hospitals))

hospital_rows = [{"hospital": hospital["name"], "icu_available": hospital["icu_available"], "ed_beds_available": hospital["ed_beds_available"], "load_percent": hospital["load_percent"], "specialties": ", ".join(hospital["specialties"])} for hospital in hospitals]
st.dataframe(hospital_rows, width="stretch", hide_index=True)
st.write("## Ambulances")
st.dataframe(ambulances, width="stretch", hide_index=True)
st.write("## Recent emergencies")
emergency_rows = [{"case": case["id"], "reported": case["reported"], "summary": case["summary"], "required_specialty": case["specialty"]} for case in emergencies]
st.dataframe(emergency_rows, width="stretch", hide_index=True)

case_label = st.selectbox("Select a recent emergency", [f"{case['id']} | {case['reported']} | {case['summary']}" for case in emergencies])
selected_case_id = case_label.split(" | ")[0]
selected_case = next(case for case in emergencies if case["id"] == selected_case_id)
triage_result = predict_priority(selected_case)
ranked = rank_hospitals(selected_case, triage_result["priority"], hospitals)
recommended = recommend_hospital(ranked)
nearest = nearest_hospital(selected_case, hospitals)
nearest_row = next(row for row in ranked if row["hospital_id"] == nearest["id"])

st.write("## AI-assisted synthetic triage")
st.metric("Predicted priority", triage_result["priority"])
st.metric("Synthetic model confidence", f"{triage_result['confidence']:.1f}%")
st.write("Most influential model factors for this small synthetic model:")
st.dataframe(triage_result["factors"], width="stretch", hide_index=True)
st.write("## Routing comparison")
st.write(f"Nearest-only result: {nearest['name']} at {selected_case['travel_minutes'][nearest['id']]} simulated minutes.")
if nearest_row["eligible"] == "No":
    st.write(f"Nearest-only failure: {nearest_row['reason']}.")
routing_rows = [{"hospital": row["hospital"], "eta_minutes": row["eta_minutes"], "eligible": row["eligible"], "icu_available": row["icu_available"], "ed_beds_available": row["ed_beds_available"], "load_percent": row["load_percent"], "route_score": "Blocked" if row["eligible"] == "No" else row["route_score"], "reason": row["reason"]} for row in ranked]
st.dataframe(routing_rows, width="stretch", hide_index=True)
if recommended:
    st.write(f"Dynamic recommendation: {recommended['hospital']}.")
else:
    st.write("No eligible hospital is available. Escalate to regional clinical and incident-command leadership before routing.")

st.write("## Regional network map")
points = []
for hospital in hospitals:
    points.append({"label": f"Hospital: {hospital['name']} | ICU: {hospital['icu_available']} | ED: {hospital['ed_beds_available']}", "coordinates": hospital["coordinates"], "color": [220, 50, 47, 190], "radius": 260})
for ambulance in ambulances:
    ambulance_color = [38, 139, 210, 200] if ambulance["status"] == "Available" else [108, 113, 196, 200]
    points.append({"label": f"Ambulance {ambulance['id']}: {ambulance['status']}", "coordinates": ambulance["coordinates"], "color": ambulance_color, "radius": 180})
for case in emergencies:
    points.append({"label": f"Emergency {case['id']}: {case['summary']}", "coordinates": case["coordinates"], "color": [255, 140, 0, 220], "radius": 220})

layers = [pdk.Layer("ScatterplotLayer", points, pickable=True, get_position="coordinates", get_fill_color="color", get_radius="radius")]
route_destination = chosen_hospital
if route_destination is None and recommended:
    route_destination = next(hospital for hospital in hospitals if hospital["id"] == recommended["hospital_id"])
if route_destination:
    route_data = [{"start": selected_case["coordinates"], "end": route_destination["coordinates"]}]
    layers.append(pdk.Layer("LineLayer", route_data, get_source_position="start", get_target_position="end", get_color=[0, 120, 255, 220], get_width=8))
network_map = pdk.Deck(layers=layers, initial_view_state=pdk.ViewState(latitude=51.5160, longitude=-0.1190, zoom=11, bearing=0, pitch=0), map_style=None, tooltip={"text": "{label}"})
st.pydeck_chart(network_map, width="stretch", height=420)

This reference combines the regional dashboard with synthetic triage results and dynamic route visualization.

You have turned a failed nearest-only choice into a transparent network recommendation. Next, you will operate the control centre by changing capacity and confirming a human-controlled dispatch.

Operate the Control Centre

The previous step gave your dashboard an explainable priority plus a capacity-aware destination. The result remains advisory until a controller can act on changing conditions.

In this step, you will add live capacity controls. You will also add human-in-the-loop control that records every dispatch.

In this step, get ready to:
  • Update hospital capacity so the ranking responds to current ICU plus ED availability.
  • Assign an available ambulance to the selected emergency.
  • Confirm a human destination decision that updates the route plus audit log.
Make hospital capacity live

A Streamlit app reruns from top to bottom after every interaction. Storing the mutable network in st.session_state preserves each capacity change across those reruns.

  • Switch back to app.py in Visual Studio Code.
  • Find the data-loading section that ends with emergencies = get_emergencies().
  • Replace that data-loading section by pasting the code below:
if "hospitals" not in st.session_state:
    st.session_state["hospitals"] = get_hospitals()
if "ambulances" not in st.session_state:
    st.session_state["ambulances"] = get_ambulances()
if "decision_log" not in st.session_state:
    st.session_state["decision_log"] = []

hospitals = st.session_state["hospitals"]
ambulances = st.session_state["ambulances"]
emergencies = get_emergencies()

How Does Live State Work?

  • The first three checks create mutable hospital data, mutable ambulance data, plus an empty decision log during the first run.
  • The local hospitals plus ambulances variables point to the saved session data during every later rerun.
  • The emergency cases remain fixed synthetic inputs, so get_emergencies() can load them normally.
  • Save app.py.
  • Switch back to the running dashboard in your browser.
  • Refresh the page to trigger a complete rerun.
  • Confirm that the network metrics still show three recent emergencies, two available ambulances, plus three open ICU beds.

Does the Dashboard Reset or Stop Loading?

  • Check that each session key uses matching quotation marks in both the initialization block plus the local assignment.
  • Confirm that get_hospitals() plus get_ambulances() only appear inside their matching first-run checks.

Help me fix Streamlit session data that resets or prevents my dashboard from loading.

Live capacity changes affect eligibility before the route score is calculated. The controller needs inputs that update the stored hospital dictionary before the dashboard builds its ranking.

  • Find the line containing st.metric("Open ICU beds", sum(hospital["icu_available"] for hospital in hospitals)).
  • Add the capacity-control block immediately below that line by pasting this code:
st.write("## Update hospital capacity")
capacity_hospital_name = st.selectbox("Hospital to update", [hospital["name"] for hospital in hospitals])
capacity_hospital = next(hospital for hospital in hospitals if hospital["name"] == capacity_hospital_name)
new_icu = st.number_input("Available ICU beds", min_value=0, max_value=20, value=int(capacity_hospital["icu_available"]), step=1, key=f"icu_{capacity_hospital['id']}")
new_ed = st.number_input("Available ED beds", min_value=0, max_value=50, value=int(capacity_hospital["ed_beds_available"]), step=1, key=f"ed_{capacity_hospital['id']}")
if st.button("Apply capacity update"):
    capacity_hospital["icu_available"] = int(new_icu)
    capacity_hospital["ed_beds_available"] = int(new_ed)
    st.session_state["last_update"] = f"Updated {capacity_hospital['name']}: ICU {int(new_icu)}, ED {int(new_ed)}"
if "last_update" in st.session_state:
    st.write(st.session_state["last_update"])

What Do the Capacity Controls Do?

  • The hospital selector finds the matching mutable dictionary inside the session network.
  • The two numeric inputs collect the current ICU plus ED availability for that hospital.
  • The button writes those values into session state before the routing section runs again.
  • The last_update message gives the controller a visible record of the applied values.
  • Save app.py.
  • Return to the running dashboard in your browser.
  • Select Central Trauma Centre under Hospital to update.
  • Enter 0 in Available ICU beds.
  • Confirm that Available ED beds still shows 4.

Before you apply the update, which hospital do you expect the dynamic ranking to recommend for E-204?

  • Click Apply capacity update.

You will see Updated Central Trauma Centre: ICU 0, ED 4. The recommendation moves to North Regional Hospital because it still has trauma capability plus an available ICU bed.

Does the Recommendation Stay on Central?

  • Confirm that the selected case is E-204.
  • Check that the update message shows an ICU value of 0 for Central Trauma Centre.
  • Confirm that the capacity block appears before the hospital table plus routing calculations in app.py.

Help me trace why changing Central Trauma Centre to zero ICU beds does not move E-204 to North Regional Hospital.

Add human dispatch controls

A recommendation becomes operational only after a controller assigns an ambulance plus approves a destination. The workflow also needs an explicit reason so an accepted route can be distinguished from an override.

  • Find the routing section that displays No eligible hospital is available. Escalate to regional clinical and incident-command leadership before routing..
  • Insert the dispatch workflow immediately before st.write("## Regional network map") by pasting this code:
st.write("## Dispatch and human decision")
ordered_ambulances = available_ambulances(selected_case, ambulances)
chosen_hospital = None
if ordered_ambulances and recommended:
    ambulance_options = [f"{ambulance['id']} | {selected_case['ambulance_eta_minutes'][ambulance['id']]} min to case" for ambulance in ordered_ambulances]
    ambulance_label = st.selectbox("Assign ambulance", ambulance_options)
    chosen_ambulance_id = ambulance_label.split(" | ")[0]
    destination_names = [row["hospital"] for row in ranked]
    default_destination = destination_names.index(recommended["hospital"])
    chosen_hospital_name = st.selectbox("Approve or override destination", destination_names, index=default_destination)
    chosen_hospital = next(hospital for hospital in hospitals if hospital["name"] == chosen_hospital_name)
    decision_reason = st.selectbox("Decision reason", ["Accepted AI-assisted recommendation", "Controller override after capability review", "Controller override after capacity update", "Escalated for clinical or incident-command review"])
    if st.button("Confirm dispatch and route"):
        chosen_ambulance = next(ambulance for ambulance in ambulances if ambulance["id"] == chosen_ambulance_id)
        chosen_ambulance["status"] = "Dispatched"
        chosen_ambulance["case_id"] = selected_case["id"]
        decision_type = "Accepted" if chosen_hospital["id"] == recommended["hospital_id"] else "Override"
        decision = {"case": selected_case["id"], "ambulance": chosen_ambulance["id"], "ai_priority": triage_result["priority"], "destination": chosen_hospital["name"], "decision": decision_type, "reason": decision_reason}
        st.session_state["decision_log"].append(decision)
        st.session_state["last_decision"] = f"{chosen_ambulance['id']} dispatched to {selected_case['id']} for transport to {chosen_hospital['name']}."
else:
    st.write("Dispatch is paused because no ambulance or eligible destination is available.")
if "last_decision" in st.session_state:
    st.write(st.session_state["last_decision"])

How Does Human Approval Work?

  • The ambulance list contains only vehicles whose current status is Available.
  • The destination selector starts on the dynamic recommendation while preserving every hospital as a controller override option.
  • Confirmation changes the chosen ambulance to Dispatched plus attaches the selected case.
  • The decision dictionary captures the case, ambulance, priority, destination, decision type, plus controller reason.
  • Save app.py.
  • Return to the running dashboard.
  • Select E-204 under Select a recent emergency.
  • Confirm that Assign ambulance shows an available vehicle.
  • Confirm that Approve or override destination defaults to North Regional Hospital.
  • Confirm that Decision reason offers an accepted recommendation plus controller override reasons.

Your controller now has an actionable recommendation plus a reasoned approval path. Leave the dispatch unconfirmed while you connect the selected destination to the map.

Are the Dispatch Selectors Missing?

  • Confirm that at least one ambulance still has an Available status.
  • Check that North Regional Hospital remains eligible after the Central ICU update.
  • Verify that the dispatch block sits after recommended plus ranked are calculated.

Help me find why my ambulance or destination selectors are missing from the dispatch section.

Connect the chosen route and audit log

The PyDeck route currently follows the automated recommendation. A controller override should move that route preview to the chosen hospital before any dispatch is confirmed.

  • Find the layers = [pdk.Layer("ScatterplotLayer", points, pickable=True, get_position="coordinates", get_fill_color="color", get_radius="radius")] line in the map section.
  • Replace the recommendation-only route block immediately below it by pasting this code:
route_destination = chosen_hospital
if route_destination is None and recommended:
    route_destination = next(hospital for hospital in hospitals if hospital["id"] == recommended["hospital_id"])
if route_destination:
    route_data = [{"start": selected_case["coordinates"], "end": route_destination["coordinates"]}]
    layers.append(pdk.Layer("LineLayer", route_data, get_source_position="start", get_target_position="end", get_color=[0, 120, 255, 220], get_width=8))

How Does the Route Choose Its Destination?

  • The route first uses chosen_hospital when the controller has selected a destination.
  • The dynamic recommendation remains the fallback before a controller selection exists.
  • The line joins the selected emergency coordinates to the active route destination.
  • Save app.py.
  • Return to the running dashboard.
  • Select Central Trauma Centre under Approve or override destination as a temporary preview.
  • Scroll to Regional network map.

You will see the route line end at Central Trauma Centre. This preview proves that the map follows the controller choice.

  • Select North Regional Hospital again under Approve or override destination.

Does the Route Ignore the Override?

  • Confirm that route_destination = chosen_hospital appears before the recommendation fallback.
  • Check that the LineLayer uses route_destination["coordinates"] for its endpoint.

Help me trace why my PyDeck route remains on the recommendation after I select an override destination.

A dispatch becomes auditable when its stored decision is visible to the controller. The final table exposes every confirmed decision made during the current session.

  • Find the st.pydeck_chart(network_map, width="stretch", height=420) line.
  • Add the audit-log display immediately below it by pasting this code:
st.write("## Decision audit log")
if st.session_state["decision_log"]:
    st.dataframe(st.session_state["decision_log"], width="stretch", hide_index=True)
else:
    st.write("No dispatch decision has been confirmed in this session.")

What Makes the Decision Auditable?

  • An empty session shows a clear message instead of an unexplained blank table.
  • Each confirmation adds one row containing the operational context plus the controller's reason.
  • The table preserves accepted recommendations plus overrides in the same reviewable format.
  • Save app.py.
  • Return to the running dashboard.
  • Select E-204 under Select a recent emergency.
  • Confirm that Central Trauma Centre shows 0 available ICU beds.
  • Select the first available vehicle under Assign ambulance.
  • Select North Regional Hospital under Approve or override destination.
  • Select Accepted AI-assisted recommendation under Decision reason.

Before you confirm the dispatch, what changes do you expect in the ambulance board plus decision log?

  • Click Confirm dispatch and route.

That closes the operational loop. You will see the selected ambulance marked Dispatched plus a new audit row for E-204 with an Immediate priority, North Regional Hospital destination, accepted decision, plus the selected reason.

Did the Dispatch Fail to Reach the Log?

  • Confirm that the dispatch button block changes the ambulance status before appending the decision.
  • Check that st.session_state["decision_log"].append(decision) remains inside the button block.
  • Verify that the audit table reads from the same decision_log session key.

Help me debug a confirmed dispatch that does not update the ambulance board or decision audit log.

✔️ Awesome, I've got everything!

Your completed app.py now supports live capacity changes, human route decisions, ambulance dispatch, map updates, plus an audit log.

ⓧ I'd like to double check the full code

import pydeck as pdk
import streamlit as st

from data import get_ambulances, get_emergencies, get_hospitals
from routing import available_ambulances, nearest_hospital, rank_hospitals, recommend_hospital
from triage import predict_priority

if "hospitals" not in st.session_state:
    st.session_state["hospitals"] = get_hospitals()
if "ambulances" not in st.session_state:
    st.session_state["ambulances"] = get_ambulances()
if "decision_log" not in st.session_state:
    st.session_state["decision_log"] = []

hospitals = st.session_state["hospitals"]
ambulances = st.session_state["ambulances"]
emergencies = get_emergencies()

st.write("# Emergency Control Centre Simulator")
st.write("Educational simulation only. All cases, hospitals, coordinates, scores, and model labels are synthetic and must not be used for patient care.")
st.write("## Live network status")
st.metric("Recent emergencies", len(emergencies))
st.metric("Available ambulances", sum(ambulance["status"] == "Available" for ambulance in ambulances))
st.metric("Open ICU beds", sum(hospital["icu_available"] for hospital in hospitals))

st.write("## Update hospital capacity")
capacity_hospital_name = st.selectbox("Hospital to update", [hospital["name"] for hospital in hospitals])
capacity_hospital = next(hospital for hospital in hospitals if hospital["name"] == capacity_hospital_name)
new_icu = st.number_input("Available ICU beds", min_value=0, max_value=20, value=int(capacity_hospital["icu_available"]), step=1, key=f"icu_{capacity_hospital['id']}")
new_ed = st.number_input("Available ED beds", min_value=0, max_value=50, value=int(capacity_hospital["ed_beds_available"]), step=1, key=f"ed_{capacity_hospital['id']}")
if st.button("Apply capacity update"):
    capacity_hospital["icu_available"] = int(new_icu)
    capacity_hospital["ed_beds_available"] = int(new_ed)
    st.session_state["last_update"] = f"Updated {capacity_hospital['name']}: ICU {int(new_icu)}, ED {int(new_ed)}"
if "last_update" in st.session_state:
    st.write(st.session_state["last_update"])

hospital_rows = [{"hospital": hospital["name"], "icu_available": hospital["icu_available"], "ed_beds_available": hospital["ed_beds_available"], "load_percent": hospital["load_percent"], "specialties": ", ".join(hospital["specialties"])} for hospital in hospitals]
st.dataframe(hospital_rows, width="stretch", hide_index=True)
st.write("## Ambulances")
st.dataframe(ambulances, width="stretch", hide_index=True)
st.write("## Recent emergencies")
emergency_rows = [{"case": case["id"], "reported": case["reported"], "summary": case["summary"], "required_specialty": case["specialty"]} for case in emergencies]
st.dataframe(emergency_rows, width="stretch", hide_index=True)

case_label = st.selectbox("Select a recent emergency", [f"{case['id']} | {case['reported']} | {case['summary']}" for case in emergencies])
selected_case_id = case_label.split(" | ")[0]
selected_case = next(case for case in emergencies if case["id"] == selected_case_id)
triage_result = predict_priority(selected_case)
ranked = rank_hospitals(selected_case, triage_result["priority"], hospitals)
recommended = recommend_hospital(ranked)
nearest = nearest_hospital(selected_case, hospitals)
nearest_row = next(row for row in ranked if row["hospital_id"] == nearest["id"])

st.write("## AI-assisted synthetic triage")
st.metric("Predicted priority", triage_result["priority"])
st.metric("Synthetic model confidence", f"{triage_result['confidence']:.1f}%")
st.write("Most influential model factors for this small synthetic model:")
st.dataframe(triage_result["factors"], width="stretch", hide_index=True)
st.write("## Routing comparison")
st.write(f"Nearest-only result: {nearest['name']} at {selected_case['travel_minutes'][nearest['id']]} simulated minutes.")
if nearest_row["eligible"] == "No":
    st.write(f"Nearest-only failure: {nearest_row['reason']}.")
routing_rows = [{"hospital": row["hospital"], "eta_minutes": row["eta_minutes"], "eligible": row["eligible"], "icu_available": row["icu_available"], "ed_beds_available": row["ed_beds_available"], "load_percent": row["load_percent"], "route_score": "Blocked" if row["eligible"] == "No" else row["route_score"], "reason": row["reason"]} for row in ranked]
st.dataframe(routing_rows, width="stretch", hide_index=True)
if recommended:
    st.write(f"Dynamic recommendation: {recommended['hospital']}.")
else:
    st.write("No eligible hospital is available. Escalate to regional clinical and incident-command leadership before routing.")

st.write("## Dispatch and human decision")
ordered_ambulances = available_ambulances(selected_case, ambulances)
chosen_hospital = None
if ordered_ambulances and recommended:
    ambulance_options = [f"{ambulance['id']} | {selected_case['ambulance_eta_minutes'][ambulance['id']]} min to case" for ambulance in ordered_ambulances]
    ambulance_label = st.selectbox("Assign ambulance", ambulance_options)
    chosen_ambulance_id = ambulance_label.split(" | ")[0]
    destination_names = [row["hospital"] for row in ranked]
    default_destination = destination_names.index(recommended["hospital"])
    chosen_hospital_name = st.selectbox("Approve or override destination", destination_names, index=default_destination)
    chosen_hospital = next(hospital for hospital in hospitals if hospital["name"] == chosen_hospital_name)
    decision_reason = st.selectbox("Decision reason", ["Accepted AI-assisted recommendation", "Controller override after capability review", "Controller override after capacity update", "Escalated for clinical or incident-command review"])
    if st.button("Confirm dispatch and route"):
        chosen_ambulance = next(ambulance for ambulance in ambulances if ambulance["id"] == chosen_ambulance_id)
        chosen_ambulance["status"] = "Dispatched"
        chosen_ambulance["case_id"] = selected_case["id"]
        decision_type = "Accepted" if chosen_hospital["id"] == recommended["hospital_id"] else "Override"
        decision = {"case": selected_case["id"], "ambulance": chosen_ambulance["id"], "ai_priority": triage_result["priority"], "destination": chosen_hospital["name"], "decision": decision_type, "reason": decision_reason}
        st.session_state["decision_log"].append(decision)
        st.session_state["last_decision"] = f"{chosen_ambulance['id']} dispatched to {selected_case['id']} for transport to {chosen_hospital['name']}."
else:
    st.write("Dispatch is paused because no ambulance or eligible destination is available.")
if "last_decision" in st.session_state:
    st.write(st.session_state["last_decision"])

st.write("## Regional network map")
points = []
for hospital in hospitals:
    points.append({"label": f"Hospital: {hospital['name']} | ICU: {hospital['icu_available']} | ED: {hospital['ed_beds_available']}", "coordinates": hospital["coordinates"], "color": [220, 50, 47, 190], "radius": 260})
for ambulance in ambulances:
    ambulance_color = [38, 139, 210, 200] if ambulance["status"] == "Available" else [108, 113, 196, 200]
    points.append({"label": f"Ambulance {ambulance['id']}: {ambulance['status']}", "coordinates": ambulance["coordinates"], "color": ambulance_color, "radius": 180})
for case in emergencies:
    points.append({"label": f"Emergency {case['id']}: {case['summary']}", "coordinates": case["coordinates"], "color": [255, 140, 0, 220], "radius": 220})

layers = [pdk.Layer("ScatterplotLayer", points, pickable=True, get_position="coordinates", get_fill_color="color", get_radius="radius")]
route_destination = chosen_hospital
if route_destination is None and recommended:
    route_destination = next(hospital for hospital in hospitals if hospital["id"] == recommended["hospital_id"])
if route_destination:
    route_data = [{"start": selected_case["coordinates"], "end": route_destination["coordinates"]}]
    layers.append(pdk.Layer("LineLayer", route_data, get_source_position="start", get_target_position="end", get_color=[0, 120, 255, 220], get_width=8))
network_map = pdk.Deck(layers=layers, initial_view_state=pdk.ViewState(latitude=51.5160, longitude=-0.1190, zoom=11, bearing=0, pitch=0), map_style=None, tooltip={"text": "{label}"})
st.pydeck_chart(network_map, width="stretch", height=420)

st.write("## Decision audit log")
if st.session_state["decision_log"]:
    st.dataframe(st.session_state["decision_log"], width="stretch", hide_index=True)
else:
    st.write("No dispatch decision has been confirmed in this session.")

Secret mission

Run an ICU Surge Drill

This drill removes ICU availability across the fictional hospital network. E-204 then tests whether the simulator pauses unsafe routing. Your final proof is a two-sentence human escalation response.

Clean Up Your Resources

Clean Up Your Resources

Your simulator runs entirely on your Windows computer, so its files and local process create no cloud charges. The options below let you preserve the live drill state, stop the local app for later, or remove the project files.

Resources you used:

  • A running local Streamlit process with a connected browser session containing the all-zero ICU drill state.
  • The emergency-control-centre folder containing app.py, data.py, requirements.txt, routing.py, and triage.py.

The Python runtime and installed packages remain on your computer because they can support future local projects.

Keep everything running

No action is needed. This option suits continued testing with the current network-wide ICU shortage.

  • Keep the Visual Studio Code terminal from earlier running.
  • Keep the browser tab connected to preserve the current session.
  • Retain the emergency-control-centre folder for further experiments.

The all-zero ICU drill state remains available through st.session_state while the browser session stays connected.

Keeping the app running creates no cloud charges.

Pause - I'll come back to this later

Stopping the local process frees the terminal while preserving every project file.

  • Switch back to the VS Code terminal from earlier.
  • Press Ctrl+C to stop the local Streamlit process.
  • Close the browser tab for the simulator.

Your terminal prompt returns when Streamlit stops. The app is no longer available in the browser.

The all-zero ICU drill values end with the session. A future session reloads the hospital defaults from data.py.

Delete - I don't want to use this again

Remove the local simulator and its source files when you have finished with the project. This option has no effect on a cloud service because the project created none.

The Keep tab preserves your code if permanent deletion feels too final.

  • Switch back to the VS Code terminal from earlier.
  • Stop the running Streamlit process by pressing Ctrl+C.
  • In the Explorer sidebar in VS Code, right-click app.py.
  • Use the file context menu to show app.py in File Explorer.
  • In the File Explorer Address bar, select the parent folder containing emergency-control-centre.
  • Select the emergency-control-centre folder.
  • Press Shift+Delete to permanently remove the folder.
  • Approve the deletion in the confirmation window.

Your local simulator and its source files are now removed from the computer. File Explorer should no longer show the emergency-control-centre folder.

Nice Work!

Nice Work!

You did it! Your browser-based Streamlit control centre now coordinates synthetic emergencies across a changing hospital network.

You've learned how to:

  • Build a live operational picture for recent emergencies. Monitor ambulance status. Compare hospital ED beds. Track ICU capacity on a regional PyDeck map.
  • Train an explainable decision tree on clearly labeled synthetic data. Classify E-204 as Immediate. Inspect the synthetic confidence. Review the most influential model factors.
  • Expose an unsafe nearest-hospital result. Recommend a suitable destination through dynamic routing. Complete an ambulance dispatch through human-in-the-loop control. Preserve the controller decision in an audit log.
  • Secret Mission: Simulate a network-wide ICU shortage with zero ICU availability at every hospital. Confirm that the simulator pauses dispatch. Document the required regional clinical escalation. Include incident-command coordination before routing.

Ready to quiz yourself?