|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
from generate_eq import create_scenario |
|
|
from generate_reports import generate_all_reports |
|
|
from update_bayesian import run_inference, compute_decision_metrics |
|
|
|
|
|
st.set_page_config(layout="wide", page_title="Earthquake Response Simulator", page_icon="🌍") |
|
|
|
|
|
st.title("Bayesian Earthquake Response Simulator") |
|
|
st.markdown("Design your own earthquake scenario and watch Bayesian inference optimize rescue decisions") |
|
|
|
|
|
with st.sidebar: |
|
|
st.header("Scenario Configuration") |
|
|
|
|
|
st.subheader("Earthquake Parameters") |
|
|
magnitude = st.slider("Magnitude (Richter)", 5.0, 8.0, 6.5, 0.1, |
|
|
help="Higher magnitude = more energy, wider damage radius") |
|
|
diameter = st.slider("Affected Area Diameter (km)", 10, 100, 40, 5) |
|
|
n_buildings = st.slider("Number of Buildings", 20, 2000, 100, 10, |
|
|
help="More buildings = more realistic but slower computation") |
|
|
n_teams = st.slider("Rescue Teams Available", 1, 50, 5, 1) |
|
|
|
|
|
st.subheader("Building Vulnerability") |
|
|
with st.expander("Advanced: Vulnerability Parameters"): |
|
|
alpha_wood = st.slider("Wood Buildings (α)", 0.5, 2.5, 1.5, 0.1) |
|
|
alpha_concrete = st.slider("Concrete Buildings (α)", 0.5, 2.5, 1.0, 0.1) |
|
|
alpha_steel = st.slider("Steel Buildings (α)", 0.5, 2.5, 0.7, 0.1) |
|
|
|
|
|
st.subheader("Report Arrival Rates") |
|
|
with st.expander("Advanced: Poisson Rates (per hour)"): |
|
|
lambda_collapse = st.slider("Collapsed Buildings", 1.0, 15.0, 8.0, 0.5) |
|
|
lambda_severe = st.slider("Severe Damage", 0.5, 8.0, 3.0, 0.5) |
|
|
lambda_minor = st.slider("Minor Damage", 0.1, 3.0, 0.8, 0.1) |
|
|
lambda_none = st.slider("No Damage", 0.05, 1.0, 0.2, 0.05) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
sim_seed = st.number_input("Random Seed", 0, 9999, 42, 1) |
|
|
|
|
|
if st.button("Generate New Scenario", type="primary"): |
|
|
st.session_state.clear() |
|
|
st.rerun() |
|
|
|
|
|
alpha_params = {'wood': alpha_wood, 'concrete': alpha_concrete, 'steel': alpha_steel} |
|
|
lambda_rates = {'collapse': lambda_collapse, 'severe': lambda_severe, 'minor': lambda_minor, 'none': lambda_none} |
|
|
|
|
|
if 'scenario' not in st.session_state: |
|
|
with st.spinner("Generating earthquake scenario..."): |
|
|
st.session_state.scenario = create_scenario( |
|
|
n_buildings=n_buildings, |
|
|
diameter_km=diameter, |
|
|
magnitude=magnitude, |
|
|
alpha_params=alpha_params, |
|
|
seed=sim_seed |
|
|
) |
|
|
st.session_state.reports = generate_all_reports( |
|
|
st.session_state.scenario, |
|
|
lambda_rates=lambda_rates, |
|
|
max_time_hours=3, |
|
|
seed=sim_seed |
|
|
) |
|
|
st.session_state.beliefs = run_inference( |
|
|
st.session_state.scenario, |
|
|
st.session_state.reports |
|
|
) |
|
|
st.session_state.metrics = compute_decision_metrics( |
|
|
st.session_state.scenario, |
|
|
st.session_state.beliefs, |
|
|
n_teams |
|
|
) |
|
|
|
|
|
scenario = st.session_state.scenario |
|
|
reports = st.session_state.reports |
|
|
beliefs = st.session_state.beliefs |
|
|
metrics = st.session_state.metrics |
|
|
|
|
|
tab1, tab2, tab3 = st.tabs(["Live Simulation", "Building Analysis", "Results & Impact"]) |
|
|
|
|
|
with tab1: |
|
|
|
|
|
|
|
|
metrics_placeholder = st.empty() |
|
|
st.divider() |
|
|
slider_placeholder = st.empty() |
|
|
|
|
|
|
|
|
with slider_placeholder: |
|
|
current_time = st.slider( |
|
|
"Simulation Time (minutes)", |
|
|
min_value=0.0, |
|
|
max_value=180.0, |
|
|
value=0.0, |
|
|
step=1.0, |
|
|
help="Slide to watch how beliefs update as reports arrive over time" |
|
|
) |
|
|
|
|
|
|
|
|
current_reports = reports[reports['time_minutes'] <= current_time] |
|
|
num_current_reports = len(current_reports) |
|
|
|
|
|
buildings_reported = current_reports['building_id'].nunique() |
|
|
|
|
|
if num_current_reports > 0: |
|
|
buildings_with_reports = current_reports['building_id'].unique() |
|
|
current_beliefs_subset = beliefs[beliefs['building_id'].isin(buildings_with_reports)] |
|
|
avg_entropy = current_beliefs_subset['entropy'].mean() |
|
|
else: |
|
|
avg_entropy = None |
|
|
|
|
|
|
|
|
with metrics_placeholder.container(): |
|
|
col2, col3, col4 = st.columns(3) |
|
|
|
|
|
with col2: |
|
|
st.metric("Reports Received", num_current_reports) |
|
|
|
|
|
with col3: |
|
|
st.metric("Buildings Reported", buildings_reported) |
|
|
|
|
|
with col4: |
|
|
if avg_entropy is not None: |
|
|
st.metric("Avg Uncertainty", f"{avg_entropy:.2f}", |
|
|
help="0 = certain, 2 = maximum uncertainty") |
|
|
else: |
|
|
st.metric("Avg Uncertainty", "—") |
|
|
|
|
|
|
|
|
from update_bayesian import bayesian_update |
|
|
from generate_reports import get_report_reliability |
|
|
|
|
|
beliefs_at_time = [] |
|
|
for _, building in scenario.iterrows(): |
|
|
prior = np.array([ |
|
|
building['p_none'], |
|
|
building['p_minor'], |
|
|
building['p_severe'], |
|
|
building['p_collapse'] |
|
|
]) |
|
|
|
|
|
building_reports = current_reports[current_reports['building_id'] == building['building_id']] |
|
|
current_belief = prior.copy() |
|
|
|
|
|
for _, report in building_reports.iterrows(): |
|
|
reliability = get_report_reliability(report['source']) |
|
|
current_belief = bayesian_update(current_belief, report['reported_state'], reliability) |
|
|
|
|
|
beliefs_at_time.append({ |
|
|
'building_id': building['building_id'], |
|
|
'p_collapse_current': current_belief[3], |
|
|
'has_reports': len(building_reports) > 0 |
|
|
}) |
|
|
|
|
|
beliefs_current_df = pd.DataFrame(beliefs_at_time) |
|
|
|
|
|
map_data = scenario[['building_id', 'x', 'y', 'distance_km', 'building_type', 'occupancy', 'true_damage']].merge( |
|
|
beliefs_current_df, |
|
|
on='building_id' |
|
|
) |
|
|
|
|
|
fig_map = px.scatter( |
|
|
map_data, |
|
|
x='x', |
|
|
y='y', |
|
|
size='occupancy', |
|
|
color='p_collapse_current', |
|
|
color_continuous_scale='RdYlGn_r', |
|
|
range_color=[0, 1], |
|
|
hover_data={ |
|
|
'building_id': True, |
|
|
'building_type': True, |
|
|
'distance_km': ':.1f', |
|
|
'occupancy': True, |
|
|
'p_collapse_current': ':.3f', |
|
|
'true_damage': True, |
|
|
'has_reports': True, |
|
|
'x': False, |
|
|
'y': False |
|
|
}, |
|
|
labels={'p_collapse_current': 'P(Collapse)', 'has_reports': 'Received Reports'}, |
|
|
title=f"Building Risk Assessment at t={int(current_time)} minutes" |
|
|
) |
|
|
|
|
|
fig_map.add_trace(go.Scatter( |
|
|
x=[0], y=[0], |
|
|
mode='markers+text', |
|
|
marker=dict(size=20, color='red', symbol='star'), |
|
|
text=['Epicenter'], |
|
|
textposition='top center', |
|
|
showlegend=False, |
|
|
hoverinfo='skip' |
|
|
)) |
|
|
|
|
|
for radius in [5, 10, 15, 20]: |
|
|
if radius < diameter / 2: |
|
|
theta = np.linspace(0, 2*np.pi, 100) |
|
|
x_circle = radius * np.cos(theta) |
|
|
y_circle = radius * np.sin(theta) |
|
|
fig_map.add_trace(go.Scatter( |
|
|
x=x_circle, y=y_circle, |
|
|
mode='lines', |
|
|
line=dict(color='gray', width=1, dash='dash'), |
|
|
showlegend=False, |
|
|
hoverinfo='skip' |
|
|
)) |
|
|
|
|
|
fig_map.update_layout( |
|
|
height=600, |
|
|
xaxis_title="Distance East-West (km)", |
|
|
yaxis_title="Distance North-South (km)", |
|
|
xaxis=dict(scaleanchor="y", scaleratio=1), |
|
|
yaxis=dict(scaleanchor="x", scaleratio=1) |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig_map, use_container_width=True) |
|
|
|
|
|
if num_current_reports > 0: |
|
|
with st.expander("Recent Reports", expanded=False): |
|
|
recent = current_reports.nsmallest(10, 'time_minutes', keep='last') |
|
|
recent = recent.sort_values('time_minutes', ascending=False) |
|
|
|
|
|
for _, report in recent.iterrows(): |
|
|
st.text( |
|
|
f"t={report['time_minutes']:6.1f}m | " |
|
|
f"Building {report['building_id']:3.0f} | " |
|
|
f"{report['source']:20s} | " |
|
|
f"Reports: {report['reported_state']:8s} | " |
|
|
f"True: {report['true_damage']}" |
|
|
) |
|
|
else: |
|
|
st.info("Move the time slider forward to see reports arrive...") |
|
|
|
|
|
with tab2: |
|
|
buildings_with_reports = reports['building_id'].unique() |
|
|
|
|
|
if len(buildings_with_reports) > 0: |
|
|
selected_building = st.selectbox( |
|
|
"Select Building", |
|
|
buildings_with_reports, |
|
|
format_func=lambda x: f"Building {x}" |
|
|
) |
|
|
|
|
|
building_info = scenario[scenario['building_id'] == selected_building].iloc[0] |
|
|
building_beliefs = beliefs[beliefs['building_id'] == selected_building].iloc[0] |
|
|
building_reports = reports[reports['building_id'] == selected_building] |
|
|
|
|
|
col1, col2 = st.columns([1, 2]) |
|
|
|
|
|
with col1: |
|
|
st.subheader("Building Information") |
|
|
st.metric("Type", building_info['building_type'].title()) |
|
|
st.metric("Distance", f"{building_info['distance_km']:.1f} km") |
|
|
st.metric("Occupancy", f"{building_info['occupancy']} people") |
|
|
st.metric("True Damage", building_info['true_damage'].title()) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
st.subheader("Inference Results") |
|
|
st.metric( |
|
|
"P(Collapse)", |
|
|
f"{building_beliefs['p_collapse']:.3f}", |
|
|
help=f"95% CI: [{building_beliefs['p_collapse_ci_lower']:.3f}, {building_beliefs['p_collapse_ci_upper']:.3f}]" |
|
|
) |
|
|
ci_width = building_beliefs['p_collapse_ci_upper'] - building_beliefs['p_collapse_ci_lower'] |
|
|
st.metric("Std Dev", f"{building_beliefs['p_collapse_std']:.3f}") |
|
|
|
|
|
st.metric("Entropy", f"{building_beliefs['entropy']:.2f}") |
|
|
st.metric("Reports Received", int(building_beliefs['num_reports'])) |
|
|
|
|
|
with col2: |
|
|
st.subheader("Belief Evolution") |
|
|
|
|
|
prior = np.array([ |
|
|
building_info['p_none'], |
|
|
building_info['p_minor'], |
|
|
building_info['p_severe'], |
|
|
building_info['p_collapse'] |
|
|
]) |
|
|
|
|
|
evolution = [{'time': 0, 'p_collapse': prior[3], 'event': 'Prior'}] |
|
|
current = prior.copy() |
|
|
|
|
|
from update_bayesian import bayesian_update |
|
|
from generate_reports import get_report_reliability |
|
|
|
|
|
for _, report in building_reports.iterrows(): |
|
|
reliability = get_report_reliability(report['source']) |
|
|
current = bayesian_update(current, report['reported_state'], reliability) |
|
|
evolution.append({ |
|
|
'time': report['time_minutes'], |
|
|
'p_collapse': current[3], |
|
|
'event': f"{report['source']}: {report['reported_state']}" |
|
|
}) |
|
|
|
|
|
evolution_df = pd.DataFrame(evolution) |
|
|
|
|
|
fig_evolution = go.Figure() |
|
|
|
|
|
fig_evolution.add_trace(go.Scatter( |
|
|
x=evolution_df['time'], |
|
|
y=evolution_df['p_collapse'], |
|
|
mode='lines+markers', |
|
|
name='P(Collapse)', |
|
|
line=dict(color='red', width=3), |
|
|
hovertemplate='%{text}<br>P(Collapse): %{y:.3f}<extra></extra>', |
|
|
text=evolution_df['event'] |
|
|
)) |
|
|
|
|
|
fig_evolution.add_hline( |
|
|
y=building_beliefs['p_collapse_ci_lower'], |
|
|
line_dash="dash", |
|
|
line_color="gray", |
|
|
annotation_text="95% CI Lower" |
|
|
) |
|
|
|
|
|
fig_evolution.add_hline( |
|
|
y=building_beliefs['p_collapse_ci_upper'], |
|
|
line_dash="dash", |
|
|
line_color="gray", |
|
|
annotation_text="95% CI Upper" |
|
|
) |
|
|
|
|
|
fig_evolution.update_layout( |
|
|
title=f"Building {selected_building} - Belief Updates Over Time", |
|
|
xaxis_title="Time (minutes)", |
|
|
yaxis_title="P(Collapse)", |
|
|
yaxis=dict(range=[0, 1]), |
|
|
height=400 |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig_evolution, use_container_width=True) |
|
|
|
|
|
st.subheader("Report Timeline") |
|
|
for _, report in building_reports.iterrows(): |
|
|
reliability = get_report_reliability(report['source']) |
|
|
st.text( |
|
|
f"t={report['time_minutes']:6.1f}m | " |
|
|
f"{report['source']:20s} ({reliability:.0%}) | " |
|
|
f"Reports: {report['reported_state']:8s} | " |
|
|
f"True: {report['true_damage']}" |
|
|
) |
|
|
else: |
|
|
st.info("No buildings received reports in this simulation. Try increasing Poisson rates.") |
|
|
|
|
|
with tab3: |
|
|
st.header("Decision Quality Comparison") |
|
|
|
|
|
improvement = metrics['improvement'] |
|
|
improvement_pct = metrics['improvement_pct'] |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
st.metric( |
|
|
"Naive Approach", |
|
|
f"{metrics['naive_lives_saved']} people", |
|
|
help="Using only distance-based priors" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
st.metric( |
|
|
"Bayesian Approach", |
|
|
f"{metrics['bayesian_lives_saved']} people", |
|
|
delta=f"+{improvement}", |
|
|
help="Using updated posterior beliefs" |
|
|
) |
|
|
|
|
|
with col3: |
|
|
st.metric( |
|
|
"Improvement", |
|
|
f"{improvement_pct:.1f}%", |
|
|
delta=f"+{improvement} lives" |
|
|
) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
if improvement > 0: |
|
|
st.success( |
|
|
f"By incorporating uncertain information via Bayesian inference, " |
|
|
f"we can reach {improvement} more people ({improvement_pct:.1f}% improvement) " |
|
|
f"with the same {n_teams} rescue teams." |
|
|
) |
|
|
elif improvement < 0: |
|
|
st.warning( |
|
|
f"In this scenario, the naive approach performed slightly better. " |
|
|
f"This can happen when reports are very noisy or when the prior is already well-calibrated." |
|
|
) |
|
|
else: |
|
|
st.info("Both approaches performed equally in this scenario.") |
|
|
|
|
|
st.subheader("Performance Metrics") |
|
|
|
|
|
map_data_full = scenario.merge(beliefs, on='building_id', suffixes=('_prior', '_posterior')) |
|
|
|
|
|
buildings_with_reports = reports['building_id'].unique() |
|
|
comparison = map_data_full[map_data_full['building_id'].isin(buildings_with_reports)].copy() |
|
|
|
|
|
if len(comparison) > 0: |
|
|
comparison['true_collapsed'] = (comparison['true_damage'] == 'collapse').astype(int) |
|
|
comparison['naive_pred'] = (comparison['p_collapse_prior'] > 0.5).astype(int) |
|
|
comparison['bayesian_pred'] = (comparison['p_collapse_posterior'] > 0.5).astype(int) |
|
|
|
|
|
naive_acc = (comparison['true_collapsed'] == comparison['naive_pred']).mean() |
|
|
bayesian_acc = (comparison['true_collapsed'] == comparison['bayesian_pred']).mean() |
|
|
|
|
|
|
|
|
st.metric("Naive Accuracy", f"{naive_acc:.1%}") |
|
|
st.metric("Bayesian Accuracy", f"{bayesian_acc:.1%}", delta=f"{bayesian_acc - naive_acc:+.1%}") |
|
|
|
|
|
st.divider() |
|
|
|