"""
Tufts Jumbo Weather Forecast — Deep Learning Demo
Usage:
cd demo && python app.py
"""
import logging
from datetime import timedelta
import gradio as gr
from hrrr_fetch import fetch_hrrr_input
from model_utils import run_forecast, load_model, AVAILABLE_MODELS
from visualization import (
get_static_maps,
plot_temperature,
plot_precipitation,
plot_wind_speed,
plot_humidity,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ── CSS ───────────────────────────────────────────────────────────────
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--font: -apple-system, BlinkMacSystemFont, "SF Pro Display",
"SF Pro Text", Inter, "Helvetica Neue", Arial, sans-serif;
--bg: #F2F2F7;
--card: #FFFFFF;
--border: #E5E5EA;
--text: #1D1D1F;
--muted: #86868B;
--accent: #0A84FF;
--dark: #1C1C1E;
}
* { font-family: var(--font) !important; }
.gradio-container {
max-width: 1320px !important;
margin: 0 auto !important;
background: var(--bg) !important;
padding-bottom: 24px !important;
}
/* ── Top bar ── */
.top-bar {
background: linear-gradient(135deg, #1C1C1E 0%, #2C2C2E 100%);
border-radius: 16px;
padding: 28px 36px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.top-bar .title {
font-size: 24px; font-weight: 700;
color: #F5F5F7; letter-spacing: -0.3px;
}
.top-bar .subtitle {
font-size: 13px; color: #98989D;
margin-top: 2px;
}
.top-bar .location {
text-align: right;
font-size: 13px; color: #98989D;
line-height: 1.6;
}
.top-bar .location b {
color: #F5F5F7; font-weight: 600;
}
/* ── Hero card ── */
.hero-card {
background: var(--card);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 32px 36px 28px;
margin-bottom: 16px;
}
.hero-main {
display: flex;
align-items: baseline;
gap: 20px;
margin-bottom: 4px;
}
.hero-temp {
font-size: 64px; font-weight: 300;
color: var(--text); letter-spacing: -2px;
line-height: 1;
}
.hero-temp-unit {
font-size: 28px; font-weight: 400;
color: var(--muted); margin-left: 2px;
}
.hero-status {
font-size: 20px; font-weight: 500;
color: var(--text); padding-left: 8px;
border-left: 3px solid var(--accent);
}
.hero-metrics {
display: flex;
gap: 12px;
margin: 20px 0 18px;
}
.metric-tile {
flex: 1;
background: var(--bg);
border-radius: 12px;
padding: 14px 16px;
text-align: center;
}
.metric-value {
font-size: 22px; font-weight: 600;
color: var(--text); line-height: 1.2;
}
.metric-label {
font-size: 12px; font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 4px;
}
.hero-meta {
font-size: 13px; color: var(--muted);
line-height: 1.6;
}
.hero-meta code {
background: var(--bg); padding: 2px 6px;
border-radius: 4px; font-size: 12px;
}
.hero-placeholder {
text-align: center;
padding: 36px 0;
color: var(--muted);
font-size: 16px; font-weight: 500;
}
/* ── Map section ── */
.maps-heading {
font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.8px;
color: var(--muted);
margin: 8px 0 8px 4px;
}
.map-cell {
background: var(--card) !important;
border-radius: 14px !important;
border: 1px solid var(--border) !important;
box-shadow: 0 1px 4px rgba(0,0,0,0.04) !important;
overflow: hidden !important;
min-height: 380px !important;
}
/* ── Controls inside hero ── */
.controls-row {
display: flex; align-items: end; gap: 10px;
margin-top: 18px; padding-top: 16px;
border-top: 1px solid var(--border);
}
/* ── Status ── */
.status-text p, .status-text em {
font-size: 12px !important; color: var(--muted) !important;
}
/* ── About ── */
.about-section {
font-size: 13px !important; color: #6E6E73 !important;
line-height: 1.65 !important;
}
/* ── Button ── */
button.primary {
background: var(--accent) !important;
border: none !important; border-radius: 10px !important;
font-weight: 600 !important; font-size: 15px !important;
padding: 10px 28px !important;
}
button.primary:hover { background: #0A74E0 !important; }
"""
# ── Helpers ────────────────────────────────────────────────────────────
model_choices = [
f"{v['display_name']} ({v['params']})" for v in AVAILABLE_MODELS.values()
]
model_keys = list(AVAILABLE_MODELS.keys())
def _resolve_model(display: str) -> str:
return model_keys[model_choices.index(display)]
def _hero_placeholder() -> str:
return (
'
'
'
'
"Click Run Forecast to fetch real-time HRRR data and generate a 24-hour prediction."
"
"
)
def _hero_html(r: dict, cycle_str: str, forecast_str: str, model_label: str) -> str:
return (
''
# temperature + status
'
'
f'
{r["temperature_c"]:.1f}'
f'°C
'
f'
{r["rain_status"]}
'
"
"
# metric tiles
'
'
f'
{r["temperature_f"]:.0f}°F
'
'
Temperature
'
f'
{r["humidity_pct"]:.0f}%
'
'
Humidity
'
f'
{r["wind_speed_ms"]:.1f}
'
f'
Wind m/s {r["wind_dir_str"]}
'
f'
{r["gust_ms"]:.1f}
'
'
Gust m/s
'
f'
{r["precipitation_mm"]:.2f}
'
'
Precip mm
'
"
"
# meta line
'
'
f"Based on {cycle_str} "
f"Forecast valid {forecast_str} "
f"Model {model_label}"
"
"
"
"
)
# ── Main callback ──────────────────────────────────────────────────────
def do_forecast(model_display: str, progress=gr.Progress()):
model_name = _resolve_model(model_display)
# Render static basemaps on first call (lazy load to avoid startup timeout)
progress(0.01, desc="Rendering basemaps...")
sat_fig, street_fig = get_static_maps()
progress(0.02, desc="Finding latest HRRR cycle...")
try:
input_array, cycle_time = fetch_hrrr_input(
progress_callback=lambda f, m: progress(f, desc=m),
)
except Exception as e:
raise gr.Error(f"HRRR fetch failed: {e}")
cycle_str = cycle_time.strftime("%Y-%m-%d %H:%M UTC")
forecast_time = cycle_time + timedelta(hours=24)
forecast_str = forecast_time.strftime("%Y-%m-%d %H:%M UTC")
progress(0.95, desc="Running model inference...")
try:
r = run_forecast(model_name, input_array)
except Exception as e:
raise gr.Error(f"Inference failed: {e}")
model_label = model_display.split("(")[0].strip()
hero = _hero_html(r, cycle_str, forecast_str, model_label)
temp_fig = plot_temperature(input_array, r, cycle_str, forecast_str)
precip_fig = plot_precipitation(input_array, r, cycle_str, forecast_str)
wind_fig = plot_wind_speed(input_array, r, cycle_str, forecast_str)
humid_fig = plot_humidity(input_array, r, cycle_str, forecast_str)
status = f"Forecast complete — HRRR cycle {cycle_str}"
return hero, sat_fig, street_fig, temp_fig, precip_fig, wind_fig, humid_fig, status
# ── Build UI ──────────────────────────────────────────────────────────
with gr.Blocks(title="Tufts Jumbo Weather Forecast", css=CUSTOM_CSS) as demo:
# ── Top bar ───────────────────────────────────────────────────
gr.HTML(
''
'
'
'
Tufts Jumbo Weather
'
'
Real-time deep-learning forecast
'
"
"
'
'
"Medford, MA
"
"42.41°N 71.12°W"
"
"
"
"
)
# ── Hero card ─────────────────────────────────────────────────
hero_html = gr.HTML(_hero_placeholder())
# ── Controls ──────────────────────────────────────────────────
with gr.Row(elem_classes=["controls-row"]):
model_dd = gr.Dropdown(
choices=model_choices, value=model_choices[0],
label="Model", scale=3,
)
run_btn = gr.Button("Run Forecast", variant="primary", scale=1)
status_bar = gr.Markdown(
"_Ready — click **Run Forecast**._",
elem_classes=["status-text"],
)
# ── Maps ──────────────────────────────────────────────────────
gr.HTML('Coverage Maps — 1 350 km × 1 350 km 3 km resolution
')
with gr.Row(equal_height=True):
sat_plot = gr.Plot(
label="Satellite",
elem_classes=["map-cell"],
)
street_plot = gr.Plot(
label="Reference Map",
elem_classes=["map-cell"],
)
temp_plot = gr.Plot(
label="Temperature",
elem_classes=["map-cell"],
)
gr.HTML('Current Input Fields with 24 h Forecast at Jumbo
')
with gr.Row(equal_height=True):
precip_plot = gr.Plot(
label="Precipitation",
elem_classes=["map-cell"],
)
wind_plot = gr.Plot(
label="Wind Speed",
elem_classes=["map-cell"],
)
humid_plot = gr.Plot(
label="Humidity",
elem_classes=["map-cell"],
)
# ── About ─────────────────────────────────────────────────────
with gr.Accordion("About this demo", open=False):
gr.Markdown(
"**Data** HRRR 3 km analysis from NOAA (AWS S3, via Herbie). "
"42 atmospheric channels covering the US Northeast.\n\n"
"**Models** CNN Baseline (11.3 M params) · ResNet-18 (11.2 M params) · "
"WeatherViT (7.4 M params, best rain AUC) — "
"predict 6 weather variables 24 h ahead for a single target point.\n\n"
"**Course** Tufts CS 137 — Deep Neural Networks, Spring 2026",
elem_classes=["about-section"],
)
# ── Callbacks ─────────────────────────────────────────────────
run_btn.click(
fn=do_forecast,
inputs=[model_dd],
outputs=[hero_html, sat_plot, street_plot, temp_plot,
precip_plot, wind_plot, humid_plot, status_bar],
)
if __name__ == "__main__":
logger.info("Pre-loading default model...")
try:
load_model(model_keys[0])
logger.info("Model loaded.")
except Exception as e:
logger.warning(f"Pre-load failed: {e}")
demo.launch(share=False)