Add side-by-side NEXRAD vs HRRR comparison with hue differentiation
Browse files- Fetch HRRR composite reflectivity images from multiple URL patterns
- Apply green-blue hue shift to HRRR data for visual differentiation
- Display NEXRAD (red/purple) and HRRR (cyan/green) simultaneously
- Add separate opacity controls for each layer (0.0-1.0)
- Support CONUS, Alaska, and Hawaii domains
- Include comparison tips and alignment checking guide
- Show data availability status for each layer
- Add numpy for image processing and color transformation
Users can now overlay both datasets with transparency to visually
validate HRRR F000 analysis alignment with real-time NEXRAD radar.
Mixed color areas indicate overlapping precipitation features.
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app.py +281 -156
- requirements.txt +1 -0
|
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
|
|
| 6 |
from io import BytesIO
|
| 7 |
from PIL import Image
|
| 8 |
import base64
|
|
|
|
| 9 |
|
| 10 |
# Domain bounds for different regions
|
| 11 |
DOMAIN_BOUNDS = {
|
|
@@ -15,56 +16,156 @@ DOMAIN_BOUNDS = {
|
|
| 15 |
'full': [[18.0, -180.0], [72.0, -66.0]]
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def get_available_runs():
|
| 19 |
-
"""
|
| 20 |
-
Generate list of recent model run times
|
| 21 |
-
HRRR runs every hour
|
| 22 |
-
"""
|
| 23 |
runs = []
|
| 24 |
now = datetime.utcnow()
|
| 25 |
current_hour = now.replace(minute=0, second=0, microsecond=0)
|
| 26 |
|
| 27 |
-
for i in range(
|
| 28 |
run_time = current_hour - timedelta(hours=i)
|
| 29 |
runs.append(run_time.strftime("%Y-%m-%d %H:00 UTC"))
|
| 30 |
|
| 31 |
return runs
|
| 32 |
|
| 33 |
-
def
|
| 34 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
legend_html = '''
|
| 36 |
<div style="position: fixed; top: 80px; right: 20px; z-index: 1000;
|
| 37 |
background-color: white; padding: 10px; border-radius: 5px;
|
| 38 |
border: 2px solid #333; font-family: Arial; font-size: 11px;
|
| 39 |
-
max-width:
|
| 40 |
<b style="font-size: 12px;">Reflectivity (dBZ)</b><br>
|
| 41 |
-
<div style="
|
| 42 |
-
<div
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
</div>
|
| 51 |
</div>
|
| 52 |
'''
|
| 53 |
return legend_html
|
| 54 |
|
| 55 |
-
def generate_map(run_time_str, forecast_hour, domain_selection,
|
|
|
|
| 56 |
"""
|
| 57 |
-
Generate Folium map with
|
| 58 |
-
|
| 59 |
-
Args:
|
| 60 |
-
run_time_str: Model run time string
|
| 61 |
-
forecast_hour: Forecast hour (0-18)
|
| 62 |
-
domain_selection: Which domain to display
|
| 63 |
-
show_nexrad: Whether to show NEXRAD real-time radar
|
| 64 |
-
show_hrrr_info: Whether to show HRRR forecast information
|
| 65 |
-
|
| 66 |
-
Returns:
|
| 67 |
-
folium.Map object
|
| 68 |
"""
|
| 69 |
# Set map center and zoom based on domain
|
| 70 |
domain_configs = {
|
|
@@ -80,85 +181,95 @@ def generate_map(run_time_str, forecast_hour, domain_selection, show_nexrad, sho
|
|
| 80 |
m = folium.Map(
|
| 81 |
location=config['location'],
|
| 82 |
zoom_start=config['zoom'],
|
| 83 |
-
tiles='
|
| 84 |
)
|
| 85 |
|
| 86 |
# Add alternative tile layers
|
| 87 |
-
folium.TileLayer('
|
| 88 |
folium.TileLayer('CartoDB dark_matter', name='Dark Map').add_to(m)
|
| 89 |
|
| 90 |
try:
|
| 91 |
-
# Parse run time
|
| 92 |
dt = datetime.strptime(run_time_str, "%Y-%m-%d %H:%M UTC")
|
| 93 |
valid_time = dt + timedelta(hours=int(forecast_hour))
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
# Add NEXRAD real-time radar if requested
|
| 96 |
if show_nexrad:
|
| 97 |
-
# NOAA MRMS (Multi-Radar Multi-Sensor) - Covers CONUS, Alaska, Hawaii, Puerto Rico
|
| 98 |
wms_url = 'https://mapservices.weather.noaa.gov/eventdriven/services/radar/radar_base_reflectivity/MapServer/WMSServer'
|
| 99 |
|
| 100 |
folium.raster_layers.WmsTileLayer(
|
| 101 |
url=wms_url,
|
| 102 |
layers='0',
|
| 103 |
-
name='NEXRAD Real-Time
|
| 104 |
format='image/png',
|
| 105 |
transparent=True,
|
| 106 |
-
opacity=
|
| 107 |
attr='NOAA',
|
| 108 |
overlay=True,
|
| 109 |
control=True
|
| 110 |
).add_to(m)
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
</div>
|
| 127 |
-
<div style='margin-top: 8px; padding: 6px; background: #e8f4f8; border-radius: 3px; font-size: 10px;'>
|
| 128 |
-
<b>About HRRR:</b> High-Resolution Rapid Refresh model provides
|
| 129 |
-
3km forecasts every hour out to 18-48 hours for CONUS, Alaska, and Hawaii.
|
| 130 |
-
</div>
|
| 131 |
-
<div style='margin-top: 6px; font-size: 9px; color: #666;'>
|
| 132 |
-
<i>Currently showing NEXRAD real-time composite reflectivity.
|
| 133 |
-
HRRR forecast data available via GRIB2 from NOAA NOMADS.</i>
|
| 134 |
-
</div>
|
| 135 |
</div>
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
font-size: 12px; max-width: 280px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);'>
|
| 148 |
-
<b style='color: #d9534f;'>π©οΈ Multi-Domain Radar Coverage</b><br>
|
| 149 |
-
<div style='margin-top: 6px; font-size: 11px;'>
|
| 150 |
-
<b>Current View:</b> {domain_selection.upper()}<br>
|
| 151 |
-
<b>Data Source:</b> {'NEXRAD Real-Time' if show_nexrad else 'None'}<br>
|
| 152 |
-
<div style='margin-top: 6px; padding: 4px; background: #fff3cd; border-left: 3px solid #ffc107; font-size: 10px;'>
|
| 153 |
-
<b>Note:</b> NEXRAD provides real-time observations
|
| 154 |
-
across CONUS, Alaska, Hawaii, and Puerto Rico.
|
| 155 |
-
</div>
|
| 156 |
</div>
|
| 157 |
</div>
|
| 158 |
"""
|
| 159 |
-
m.get_root().html.add_child(folium.Element(
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
-
# Add domain boundary
|
| 162 |
if domain_selection in DOMAIN_BOUNDS:
|
| 163 |
bounds = DOMAIN_BOUNDS[domain_selection]
|
| 164 |
folium.Rectangle(
|
|
@@ -170,14 +281,12 @@ def generate_map(run_time_str, forecast_hour, domain_selection, show_nexrad, sho
|
|
| 170 |
).add_to(m)
|
| 171 |
|
| 172 |
except Exception as e:
|
| 173 |
-
# Add error message
|
| 174 |
error_html = f"""
|
| 175 |
<div style='position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 176 |
z-index: 1001; background-color: #ffcccc; padding: 20px;
|
| 177 |
border-radius: 10px; border: 2px solid #ff0000; font-family: Arial;'>
|
| 178 |
<h3 style='margin-top: 0;'>Error Loading Data</h3>
|
| 179 |
<p>Error: {str(e)}</p>
|
| 180 |
-
<p>Please try different settings.</p>
|
| 181 |
</div>
|
| 182 |
"""
|
| 183 |
m.get_root().html.add_child(folium.Element(error_html))
|
|
@@ -185,10 +294,8 @@ def generate_map(run_time_str, forecast_hour, domain_selection, show_nexrad, sho
|
|
| 185 |
# Add layer control
|
| 186 |
folium.LayerControl(position='topright', collapsed=False).add_to(m)
|
| 187 |
|
| 188 |
-
# Add fullscreen
|
| 189 |
plugins.Fullscreen(position='topleft').add_to(m)
|
| 190 |
-
|
| 191 |
-
# Add measure control
|
| 192 |
plugins.MeasureControl(position='bottomright', primary_length_unit='miles').add_to(m)
|
| 193 |
|
| 194 |
return m
|
|
@@ -196,14 +303,14 @@ def generate_map(run_time_str, forecast_hour, domain_selection, show_nexrad, sho
|
|
| 196 |
def create_interface():
|
| 197 |
"""Create Gradio interface"""
|
| 198 |
|
| 199 |
-
with gr.Blocks(title="HRRR
|
| 200 |
gr.Markdown("""
|
| 201 |
-
# π©οΈ HRRR
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
|
| 206 |
-
**Data
|
| 207 |
""")
|
| 208 |
|
| 209 |
with gr.Row():
|
|
@@ -212,7 +319,7 @@ def create_interface():
|
|
| 212 |
choices=get_available_runs(),
|
| 213 |
value=get_available_runs()[0],
|
| 214 |
label="π Model Run Time (UTC)",
|
| 215 |
-
info="HRRR
|
| 216 |
)
|
| 217 |
|
| 218 |
with gr.Column(scale=1):
|
|
@@ -222,119 +329,137 @@ def create_interface():
|
|
| 222 |
step=1,
|
| 223 |
value=0,
|
| 224 |
label="β±οΈ Forecast Hour",
|
| 225 |
-
info="F000 = Analysis
|
| 226 |
)
|
| 227 |
|
| 228 |
with gr.Column(scale=1):
|
| 229 |
domain = gr.Radio(
|
| 230 |
-
choices=['
|
| 231 |
value='conus',
|
| 232 |
label="πΊοΈ Domain",
|
| 233 |
-
info="
|
| 234 |
)
|
| 235 |
|
|
|
|
|
|
|
| 236 |
with gr.Row():
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
-
load_btn = gr.Button("π Load
|
| 250 |
|
| 251 |
with gr.Row():
|
| 252 |
-
map_output = gr.HTML(label="
|
| 253 |
|
| 254 |
-
def load_map(run_time, forecast_hour, domain, show_nexrad,
|
| 255 |
-
m = generate_map(run_time, int(forecast_hour), domain,
|
|
|
|
| 256 |
return m._repr_html_()
|
| 257 |
|
| 258 |
load_btn.click(
|
| 259 |
fn=load_map,
|
| 260 |
-
inputs=[run_time, forecast_hour, domain, show_nexrad,
|
| 261 |
outputs=map_output
|
| 262 |
)
|
| 263 |
|
| 264 |
# Auto-load on startup
|
| 265 |
demo.load(
|
| 266 |
fn=load_map,
|
| 267 |
-
inputs=[run_time, forecast_hour, domain, show_nexrad,
|
| 268 |
outputs=map_output
|
| 269 |
)
|
| 270 |
|
| 271 |
gr.Markdown("""
|
| 272 |
---
|
| 273 |
-
## π
|
| 274 |
-
|
| 275 |
-
The **High-Resolution Rapid Refresh (HRRR)** is NOAA's high-resolution, short-range weather model that provides
|
| 276 |
-
3km grid spacing forecasts updated every hour.
|
| 277 |
|
| 278 |
-
###
|
| 279 |
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
-
###
|
| 285 |
|
| 286 |
-
|
| 287 |
-
-
|
| 288 |
-
- **
|
| 289 |
-
- **40-50 dBZ**: Heavy precipitation
|
| 290 |
-
- **30-40 dBZ**: Moderate to heavy rain
|
| 291 |
-
- **20-30 dBZ**: Light to moderate rain
|
| 292 |
-
- **10-20 dBZ**: Light rain
|
| 293 |
-
- **<10 dBZ**: Very light precipitation
|
| 294 |
|
| 295 |
-
###
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
|
| 304 |
-
###
|
| 305 |
|
| 306 |
-
- **F000
|
| 307 |
-
- **
|
| 308 |
-
- **
|
|
|
|
| 309 |
|
| 310 |
-
###
|
| 311 |
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
4. **Use Case**: Compare forecast hours (F001-F018) with F000/NEXRAD for model validation
|
| 316 |
|
| 317 |
-
###
|
| 318 |
|
| 319 |
-
**HRRR
|
| 320 |
-
-
|
| 321 |
-
-
|
|
|
|
| 322 |
|
| 323 |
-
**NEXRAD
|
| 324 |
-
-
|
| 325 |
-
-
|
|
|
|
| 326 |
|
| 327 |
-
###
|
| 328 |
|
| 329 |
-
- [HRRR Information
|
| 330 |
-
- [
|
| 331 |
-
- [
|
| 332 |
-
- [NEXRAD Radar Network](https://www.ncei.noaa.gov/products/radar/next-generation-weather-radar)
|
| 333 |
|
| 334 |
---
|
| 335 |
|
| 336 |
<p style='text-align: center; color: #666; font-size: 11px;'>
|
| 337 |
-
Data
|
| 338 |
</p>
|
| 339 |
""")
|
| 340 |
|
|
|
|
| 6 |
from io import BytesIO
|
| 7 |
from PIL import Image
|
| 8 |
import base64
|
| 9 |
+
import numpy as np
|
| 10 |
|
| 11 |
# Domain bounds for different regions
|
| 12 |
DOMAIN_BOUNDS = {
|
|
|
|
| 16 |
'full': [[18.0, -180.0], [72.0, -66.0]]
|
| 17 |
}
|
| 18 |
|
| 19 |
+
# HRRR image bounds (approximate for composite reflectivity images)
|
| 20 |
+
HRRR_IMAGE_BOUNDS = {
|
| 21 |
+
'conus': [[20.0, -130.0], [52.0, -60.0]],
|
| 22 |
+
'alaska': [[48.0, -180.0], [75.0, -125.0]],
|
| 23 |
+
'hawaii': [[17.0, -162.0], [24.0, -153.0]]
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
def get_available_runs():
|
| 27 |
+
"""Generate list of recent model run times"""
|
|
|
|
|
|
|
|
|
|
| 28 |
runs = []
|
| 29 |
now = datetime.utcnow()
|
| 30 |
current_hour = now.replace(minute=0, second=0, microsecond=0)
|
| 31 |
|
| 32 |
+
for i in range(48): # Last 48 hours
|
| 33 |
run_time = current_hour - timedelta(hours=i)
|
| 34 |
runs.append(run_time.strftime("%Y-%m-%d %H:00 UTC"))
|
| 35 |
|
| 36 |
return runs
|
| 37 |
|
| 38 |
+
def try_fetch_hrrr_image(run_time_str, forecast_hour, domain='conus'):
|
| 39 |
+
"""
|
| 40 |
+
Try to fetch HRRR composite reflectivity image from various NOAA sources
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
PIL Image object if found, None otherwise
|
| 44 |
+
"""
|
| 45 |
+
dt = datetime.strptime(run_time_str, "%Y-%m-%d %H:%M UTC")
|
| 46 |
+
run_str = dt.strftime("%Y%m%d%H")
|
| 47 |
+
|
| 48 |
+
# Map domain to HRRR naming
|
| 49 |
+
domain_map = {
|
| 50 |
+
'conus': 'conus',
|
| 51 |
+
'alaska': 'alaska',
|
| 52 |
+
'hawaii': 'hawaii',
|
| 53 |
+
'full': 'conus' # Start with conus for full view
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
hrrr_domain = domain_map.get(domain, 'conus')
|
| 57 |
+
|
| 58 |
+
# Try multiple URL patterns for HRRR composite reflectivity
|
| 59 |
+
url_patterns = [
|
| 60 |
+
# Pattern 1: Standard for_web structure
|
| 61 |
+
f"https://rapidrefresh.noaa.gov/hrrr/HRRR/for_web/hrrr_ncep_jet/{run_str}/{hrrr_domain}/refc_sfc_f{forecast_hour:02d}.png",
|
| 62 |
+
# Pattern 2: Alternative structure
|
| 63 |
+
f"https://rapidrefresh.noaa.gov/hrrr/HRRR/for_web/hrrr_{hrrr_domain}/{run_str}/refc_sfc_f{forecast_hour:02d}.png",
|
| 64 |
+
# Pattern 3: Simplified path
|
| 65 |
+
f"https://rapidrefresh.noaa.gov/hrrr/for_web/{run_str}/{hrrr_domain}/refc_sfc_f{forecast_hour:02d}.png",
|
| 66 |
+
# Pattern 4: Direct HRRR graphics
|
| 67 |
+
f"https://rapidrefresh.noaa.gov/hrrr/HRRR/displayMapLocalDiskDateDomainZipTZModel.cgi?keys=hrrr_ncep_jet:&runtime={run_str}&plot_type=refc&fcst={forecast_hour:02d}",
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
for url in url_patterns:
|
| 71 |
+
try:
|
| 72 |
+
response = requests.get(url, timeout=10)
|
| 73 |
+
if response.status_code == 200 and len(response.content) > 1000: # Valid image
|
| 74 |
+
img = Image.open(BytesIO(response.content))
|
| 75 |
+
return img, url
|
| 76 |
+
except:
|
| 77 |
+
continue
|
| 78 |
+
|
| 79 |
+
return None, None
|
| 80 |
+
|
| 81 |
+
def apply_hue_shift(image, hue_shift=0.3):
|
| 82 |
+
"""
|
| 83 |
+
Apply hue shift to image to differentiate from NEXRAD
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
image: PIL Image
|
| 87 |
+
hue_shift: Hue shift amount (0-1), 0.3 = greenish-blue tint
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
PIL Image with hue shifted
|
| 91 |
+
"""
|
| 92 |
+
# Convert to RGBA if not already
|
| 93 |
+
if image.mode != 'RGBA':
|
| 94 |
+
image = image.convert('RGBA')
|
| 95 |
+
|
| 96 |
+
# Convert to numpy array
|
| 97 |
+
img_array = np.array(image)
|
| 98 |
+
|
| 99 |
+
# Separate RGB and alpha channels
|
| 100 |
+
rgb = img_array[:, :, :3].astype(float)
|
| 101 |
+
alpha = img_array[:, :, 3]
|
| 102 |
+
|
| 103 |
+
# Convert RGB to HSV
|
| 104 |
+
rgb_normalized = rgb / 255.0
|
| 105 |
+
|
| 106 |
+
# Simple hue shift by rotating RGB values
|
| 107 |
+
# This gives a greenish-blue tint to HRRR data
|
| 108 |
+
r, g, b = rgb_normalized[:,:,0], rgb_normalized[:,:,1], rgb_normalized[:,:,2]
|
| 109 |
+
|
| 110 |
+
# Apply color tint - shift toward cyan/green for HRRR
|
| 111 |
+
r_new = r * 0.6 + g * 0.2 + b * 0.2 # Reduce red
|
| 112 |
+
g_new = r * 0.1 + g * 0.8 + b * 0.1 # Enhance green
|
| 113 |
+
b_new = r * 0.1 + g * 0.3 + b * 0.6 # Moderate blue
|
| 114 |
+
|
| 115 |
+
# Stack and convert back
|
| 116 |
+
rgb_shifted = np.stack([r_new, g_new, b_new], axis=2)
|
| 117 |
+
rgb_shifted = np.clip(rgb_shifted * 255, 0, 255).astype(np.uint8)
|
| 118 |
+
|
| 119 |
+
# Recombine with alpha
|
| 120 |
+
img_shifted = np.dstack([rgb_shifted, alpha])
|
| 121 |
+
|
| 122 |
+
return Image.fromarray(img_shifted, 'RGBA')
|
| 123 |
+
|
| 124 |
+
def image_to_data_url(image):
|
| 125 |
+
"""Convert PIL Image to data URL for folium"""
|
| 126 |
+
buffered = BytesIO()
|
| 127 |
+
image.save(buffered, format="PNG")
|
| 128 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
| 129 |
+
return f"data:image/png;base64,{img_str}"
|
| 130 |
+
|
| 131 |
+
def create_legends():
|
| 132 |
+
"""Create HTML legends for both radar types"""
|
| 133 |
legend_html = '''
|
| 134 |
<div style="position: fixed; top: 80px; right: 20px; z-index: 1000;
|
| 135 |
background-color: white; padding: 10px; border-radius: 5px;
|
| 136 |
border: 2px solid #333; font-family: Arial; font-size: 11px;
|
| 137 |
+
max-width: 200px;">
|
| 138 |
<b style="font-size: 12px;">Reflectivity (dBZ)</b><br>
|
| 139 |
+
<div style="margin-top: 8px;">
|
| 140 |
+
<div style="display: flex; gap: 8px; margin-bottom: 6px;">
|
| 141 |
+
<div style="flex: 1;">
|
| 142 |
+
<b style="font-size: 10px; color: #d9534f;">NEXRAD</b><br>
|
| 143 |
+
<div style="margin-top: 3px; font-size: 9px;">Real-time<br>(Standard colors)</div>
|
| 144 |
+
</div>
|
| 145 |
+
<div style="flex: 1;">
|
| 146 |
+
<b style="font-size: 10px; color: #5cb85c;">HRRR</b><br>
|
| 147 |
+
<div style="margin-top: 3px; font-size: 9px;">Forecast<br>(Green-blue tint)</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
<div style="display: flex; flex-direction: column; margin-top: 8px; gap: 2px; border-top: 1px solid #ccc; padding-top: 6px;">
|
| 152 |
+
<div style="font-size: 10px;"><span style="background: #9854c6; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span>60+ (Extreme)</div>
|
| 153 |
+
<div style="font-size: 10px;"><span style="background: #f800fd; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span>50-60 (Severe)</div>
|
| 154 |
+
<div style="font-size: 10px;"><span style="background: #bc0000; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span>40-50 (Heavy)</div>
|
| 155 |
+
<div style="font-size: 10px;"><span style="background: #fd0000; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span>30-40 (Moderate)</div>
|
| 156 |
+
<div style="font-size: 10px;"><span style="background: #fd9500; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span>25-30 (Light)</div>
|
| 157 |
+
<div style="font-size: 10px;"><span style="background: #fdf802; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span>20-25</div>
|
| 158 |
+
<div style="font-size: 10px;"><span style="background: #02fd02; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span>10-20 (Weak)</div>
|
| 159 |
+
<div style="font-size: 10px;"><span style="background: #019ff4; width: 16px; height: 8px; display: inline-block; margin-right: 4px;"></span><10 (Trace)</div>
|
| 160 |
</div>
|
| 161 |
</div>
|
| 162 |
'''
|
| 163 |
return legend_html
|
| 164 |
|
| 165 |
+
def generate_map(run_time_str, forecast_hour, domain_selection,
|
| 166 |
+
show_nexrad, nexrad_opacity, show_hrrr, hrrr_opacity):
|
| 167 |
"""
|
| 168 |
+
Generate Folium map with both NEXRAD and HRRR overlays for comparison
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
"""
|
| 170 |
# Set map center and zoom based on domain
|
| 171 |
domain_configs = {
|
|
|
|
| 181 |
m = folium.Map(
|
| 182 |
location=config['location'],
|
| 183 |
zoom_start=config['zoom'],
|
| 184 |
+
tiles='CartoDB positron'
|
| 185 |
)
|
| 186 |
|
| 187 |
# Add alternative tile layers
|
| 188 |
+
folium.TileLayer('OpenStreetMap', name='Street Map').add_to(m)
|
| 189 |
folium.TileLayer('CartoDB dark_matter', name='Dark Map').add_to(m)
|
| 190 |
|
| 191 |
try:
|
|
|
|
| 192 |
dt = datetime.strptime(run_time_str, "%Y-%m-%d %H:%M UTC")
|
| 193 |
valid_time = dt + timedelta(hours=int(forecast_hour))
|
| 194 |
|
| 195 |
+
data_status = []
|
| 196 |
+
|
| 197 |
+
# Add HRRR forecast overlay if requested
|
| 198 |
+
if show_hrrr:
|
| 199 |
+
hrrr_img, hrrr_url = try_fetch_hrrr_image(run_time_str, int(forecast_hour), domain_selection)
|
| 200 |
+
|
| 201 |
+
if hrrr_img:
|
| 202 |
+
# Apply hue shift to HRRR data (greenish-blue tint)
|
| 203 |
+
hrrr_shifted = apply_hue_shift(hrrr_img)
|
| 204 |
+
hrrr_data_url = image_to_data_url(hrrr_shifted)
|
| 205 |
+
|
| 206 |
+
# Get bounds for this domain
|
| 207 |
+
bounds = HRRR_IMAGE_BOUNDS.get(domain_selection, HRRR_IMAGE_BOUNDS['conus'])
|
| 208 |
+
|
| 209 |
+
folium.raster_layers.ImageOverlay(
|
| 210 |
+
image=hrrr_data_url,
|
| 211 |
+
bounds=bounds,
|
| 212 |
+
opacity=hrrr_opacity,
|
| 213 |
+
name='HRRR Forecast (Green-Blue Tint)',
|
| 214 |
+
overlay=True,
|
| 215 |
+
control=True
|
| 216 |
+
).add_to(m)
|
| 217 |
+
|
| 218 |
+
data_status.append(f"β HRRR F{int(forecast_hour):03d} loaded")
|
| 219 |
+
else:
|
| 220 |
+
data_status.append(f"β HRRR F{int(forecast_hour):03d} not available")
|
| 221 |
+
|
| 222 |
# Add NEXRAD real-time radar if requested
|
| 223 |
if show_nexrad:
|
|
|
|
| 224 |
wms_url = 'https://mapservices.weather.noaa.gov/eventdriven/services/radar/radar_base_reflectivity/MapServer/WMSServer'
|
| 225 |
|
| 226 |
folium.raster_layers.WmsTileLayer(
|
| 227 |
url=wms_url,
|
| 228 |
layers='0',
|
| 229 |
+
name='NEXRAD Real-Time (Standard Colors)',
|
| 230 |
format='image/png',
|
| 231 |
transparent=True,
|
| 232 |
+
opacity=nexrad_opacity,
|
| 233 |
attr='NOAA',
|
| 234 |
overlay=True,
|
| 235 |
control=True
|
| 236 |
).add_to(m)
|
| 237 |
|
| 238 |
+
data_status.append("β NEXRAD Real-Time loaded")
|
| 239 |
+
|
| 240 |
+
# Add comparison info box
|
| 241 |
+
comparison_html = f"""
|
| 242 |
+
<div style='position: fixed; bottom: 20px; left: 20px; z-index: 1000;
|
| 243 |
+
background-color: rgba(255, 255, 255, 0.95); padding: 12px;
|
| 244 |
+
border-radius: 5px; border: 2px solid #0066cc; font-family: Arial;
|
| 245 |
+
max-width: 340px; box-shadow: 0 2px 5px rgba(0,0,0,0.3);'>
|
| 246 |
+
<b style='color: #0066cc; font-size: 14px;'>π Data Comparison View</b><br>
|
| 247 |
+
<div style='margin-top: 8px; font-size: 11px;'>
|
| 248 |
+
<b>Model Run:</b> {dt.strftime("%Y-%m-%d %H:00 UTC")}<br>
|
| 249 |
+
<b>Forecast Hour:</b> F{int(forecast_hour):03d}<br>
|
| 250 |
+
<b>Valid Time:</b> {valid_time.strftime("%Y-%m-%d %H:00 UTC")}<br>
|
| 251 |
+
<b>Domain:</b> {domain_selection.upper()}<br>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
</div>
|
| 253 |
+
<div style='margin-top: 8px; padding: 6px; background: #e8f4f8; border-radius: 3px; font-size: 10px;'>
|
| 254 |
+
<b>Data Status:</b><br>
|
| 255 |
+
{'<br>'.join(data_status) if data_status else 'No data layers selected'}
|
| 256 |
+
</div>
|
| 257 |
+
<div style='margin-top: 6px; padding: 6px; background: #fff3cd; border-radius: 3px; font-size: 9px;'>
|
| 258 |
+
<b>π‘ Comparison Tips:</b><br>
|
| 259 |
+
β’ <span style="color: #d9534f;">Red/Purple</span> = NEXRAD standard colors<br>
|
| 260 |
+
β’ <span style="color: #5cb85c;">Green/Cyan</span> = HRRR with color shift<br>
|
| 261 |
+
β’ Overlapping areas appear <b>mixed</b><br>
|
| 262 |
+
β’ Perfect alignment = good model performance<br>
|
| 263 |
+
β’ Use opacity sliders to adjust visibility
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
</div>
|
| 265 |
</div>
|
| 266 |
"""
|
| 267 |
+
m.get_root().html.add_child(folium.Element(comparison_html))
|
| 268 |
+
|
| 269 |
+
# Add legend
|
| 270 |
+
m.get_root().html.add_child(folium.Element(create_legends()))
|
| 271 |
|
| 272 |
+
# Add domain boundary
|
| 273 |
if domain_selection in DOMAIN_BOUNDS:
|
| 274 |
bounds = DOMAIN_BOUNDS[domain_selection]
|
| 275 |
folium.Rectangle(
|
|
|
|
| 281 |
).add_to(m)
|
| 282 |
|
| 283 |
except Exception as e:
|
|
|
|
| 284 |
error_html = f"""
|
| 285 |
<div style='position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 286 |
z-index: 1001; background-color: #ffcccc; padding: 20px;
|
| 287 |
border-radius: 10px; border: 2px solid #ff0000; font-family: Arial;'>
|
| 288 |
<h3 style='margin-top: 0;'>Error Loading Data</h3>
|
| 289 |
<p>Error: {str(e)}</p>
|
|
|
|
| 290 |
</div>
|
| 291 |
"""
|
| 292 |
m.get_root().html.add_child(folium.Element(error_html))
|
|
|
|
| 294 |
# Add layer control
|
| 295 |
folium.LayerControl(position='topright', collapsed=False).add_to(m)
|
| 296 |
|
| 297 |
+
# Add fullscreen and measure controls
|
| 298 |
plugins.Fullscreen(position='topleft').add_to(m)
|
|
|
|
|
|
|
| 299 |
plugins.MeasureControl(position='bottomright', primary_length_unit='miles').add_to(m)
|
| 300 |
|
| 301 |
return m
|
|
|
|
| 303 |
def create_interface():
|
| 304 |
"""Create Gradio interface"""
|
| 305 |
|
| 306 |
+
with gr.Blocks(title="HRRR vs NEXRAD Radar Comparison", theme=gr.themes.Soft()) as demo:
|
| 307 |
gr.Markdown("""
|
| 308 |
+
# π©οΈ HRRR vs NEXRAD Radar Comparison Viewer
|
| 309 |
|
| 310 |
+
Compare NOAA HRRR forecast composite reflectivity with real-time NEXRAD radar data.
|
| 311 |
+
**Both layers shown simultaneously with different colors for visual alignment checking.**
|
| 312 |
|
| 313 |
+
**Data Sources:** NOAA NEXRAD Real-Time Radar (standard colors) + HRRR Model Forecast (green-blue tint)
|
| 314 |
""")
|
| 315 |
|
| 316 |
with gr.Row():
|
|
|
|
| 319 |
choices=get_available_runs(),
|
| 320 |
value=get_available_runs()[0],
|
| 321 |
label="π Model Run Time (UTC)",
|
| 322 |
+
info="HRRR initialization time"
|
| 323 |
)
|
| 324 |
|
| 325 |
with gr.Column(scale=1):
|
|
|
|
| 329 |
step=1,
|
| 330 |
value=0,
|
| 331 |
label="β±οΈ Forecast Hour",
|
| 332 |
+
info="F000 = Analysis (best for comparison)"
|
| 333 |
)
|
| 334 |
|
| 335 |
with gr.Column(scale=1):
|
| 336 |
domain = gr.Radio(
|
| 337 |
+
choices=['conus', 'alaska', 'hawaii'],
|
| 338 |
value='conus',
|
| 339 |
label="πΊοΈ Domain",
|
| 340 |
+
info="Geographic region"
|
| 341 |
)
|
| 342 |
|
| 343 |
+
gr.Markdown("### π¨ Layer Controls - Adjust to Compare Alignment")
|
| 344 |
+
|
| 345 |
with gr.Row():
|
| 346 |
+
with gr.Column(scale=1):
|
| 347 |
+
show_nexrad = gr.Checkbox(
|
| 348 |
+
value=True,
|
| 349 |
+
label="π‘ Show NEXRAD Real-Time",
|
| 350 |
+
info="Standard red/purple colors"
|
| 351 |
+
)
|
| 352 |
+
nexrad_opacity = gr.Slider(
|
| 353 |
+
minimum=0.0,
|
| 354 |
+
maximum=1.0,
|
| 355 |
+
value=0.6,
|
| 356 |
+
step=0.1,
|
| 357 |
+
label="NEXRAD Opacity",
|
| 358 |
+
info="Lower to see HRRR underneath"
|
| 359 |
+
)
|
| 360 |
|
| 361 |
+
with gr.Column(scale=1):
|
| 362 |
+
show_hrrr = gr.Checkbox(
|
| 363 |
+
value=True,
|
| 364 |
+
label="π°οΈ Show HRRR Forecast",
|
| 365 |
+
info="Green/cyan color tint"
|
| 366 |
+
)
|
| 367 |
+
hrrr_opacity = gr.Slider(
|
| 368 |
+
minimum=0.0,
|
| 369 |
+
maximum=1.0,
|
| 370 |
+
value=0.5,
|
| 371 |
+
step=0.1,
|
| 372 |
+
label="HRRR Opacity",
|
| 373 |
+
info="Adjust for blending"
|
| 374 |
+
)
|
| 375 |
|
| 376 |
+
load_btn = gr.Button("π Load Comparison View", variant="primary", size="lg")
|
| 377 |
|
| 378 |
with gr.Row():
|
| 379 |
+
map_output = gr.HTML(label="Comparison Map")
|
| 380 |
|
| 381 |
+
def load_map(run_time, forecast_hour, domain, show_nexrad, nexrad_opacity, show_hrrr, hrrr_opacity):
|
| 382 |
+
m = generate_map(run_time, int(forecast_hour), domain,
|
| 383 |
+
show_nexrad, nexrad_opacity, show_hrrr, hrrr_opacity)
|
| 384 |
return m._repr_html_()
|
| 385 |
|
| 386 |
load_btn.click(
|
| 387 |
fn=load_map,
|
| 388 |
+
inputs=[run_time, forecast_hour, domain, show_nexrad, nexrad_opacity, show_hrrr, hrrr_opacity],
|
| 389 |
outputs=map_output
|
| 390 |
)
|
| 391 |
|
| 392 |
# Auto-load on startup
|
| 393 |
demo.load(
|
| 394 |
fn=load_map,
|
| 395 |
+
inputs=[run_time, forecast_hour, domain, show_nexrad, nexrad_opacity, show_hrrr, hrrr_opacity],
|
| 396 |
outputs=map_output
|
| 397 |
)
|
| 398 |
|
| 399 |
gr.Markdown("""
|
| 400 |
---
|
| 401 |
+
## π How to Use This Comparison Tool
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
+
### Visual Alignment Check
|
| 404 |
|
| 405 |
+
1. **Set Forecast Hour to 0** (F000 = HRRR analysis)
|
| 406 |
+
2. **Enable both NEXRAD and HRRR** layers
|
| 407 |
+
3. **Adjust opacity sliders** to see both layers clearly
|
| 408 |
+
4. **Look for alignment:**
|
| 409 |
+
- **Perfect overlap** = HRRR correctly assimilated radar data
|
| 410 |
+
- **Offset/misalignment** = potential data issues or timing differences
|
| 411 |
+
- **Different intensities** = model vs. observation differences
|
| 412 |
|
| 413 |
+
### Color Coding
|
| 414 |
|
| 415 |
+
- **π΄ NEXRAD (Standard Colors):** Red, purple, yellow = Real-time radar observations
|
| 416 |
+
- **π’ HRRR (Green-Blue Tint):** Cyan, green = Model forecast with color shift
|
| 417 |
+
- **Mixed Areas:** Where both overlap, you'll see blended colors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
|
| 419 |
+
### Recommended Settings for Comparison
|
| 420 |
|
| 421 |
+
| Purpose | NEXRAD Opacity | HRRR Opacity | Notes |
|
| 422 |
+
|---------|---------------|--------------|-------|
|
| 423 |
+
| Check alignment | 0.6 | 0.5 | Balanced visibility |
|
| 424 |
+
| Focus on NEXRAD | 0.8 | 0.3 | HRRR as reference |
|
| 425 |
+
| Focus on HRRR | 0.3 | 0.7 | NEXRAD as reference |
|
| 426 |
+
| See differences | 0.5 | 0.5 | Equal blending |
|
| 427 |
|
| 428 |
+
### Understanding Forecast Hours
|
| 429 |
|
| 430 |
+
- **F000**: HRRR analysis - should match NEXRAD closely (uses radar data assimilation)
|
| 431 |
+
- **F001-F003**: Very short-term forecast - minor divergence expected
|
| 432 |
+
- **F006-F012**: Short-term forecast - moderate divergence
|
| 433 |
+
- **F012-F018**: Medium-range forecast - larger divergence from real-time
|
| 434 |
|
| 435 |
+
### Model Coverage
|
| 436 |
|
| 437 |
+
- **CONUS HRRR**: Continental US at 3km resolution, updated hourly
|
| 438 |
+
- **Alaska HRRR**: Alaska domain at 3km resolution
|
| 439 |
+
- **Hawaii HRRR**: Hawaiian Islands at 3km resolution
|
|
|
|
| 440 |
|
| 441 |
+
### Data Availability
|
| 442 |
|
| 443 |
+
**HRRR Data:**
|
| 444 |
+
- Images may not always be available from rapidrefresh.noaa.gov
|
| 445 |
+
- Try recent run times (last 6-12 hours) for best availability
|
| 446 |
+
- GRIB2 data always available from NOMADS/AWS S3
|
| 447 |
|
| 448 |
+
**NEXRAD Data:**
|
| 449 |
+
- Real-time WMS service, always current
|
| 450 |
+
- Updates every ~5 minutes
|
| 451 |
+
- Covers CONUS, Alaska, Hawaii, Puerto Rico
|
| 452 |
|
| 453 |
+
### π References
|
| 454 |
|
| 455 |
+
- [HRRR Information](https://rapidrefresh.noaa.gov/hrrr/)
|
| 456 |
+
- [NEXRAD Documentation](https://www.ncei.noaa.gov/products/radar/next-generation-weather-radar)
|
| 457 |
+
- [HRRR on AWS](https://registry.opendata.aws/noaa-hrrr/)
|
|
|
|
| 458 |
|
| 459 |
---
|
| 460 |
|
| 461 |
<p style='text-align: center; color: #666; font-size: 11px;'>
|
| 462 |
+
Data: NOAA | For research/educational purposes only | Not for operational use
|
| 463 |
</p>
|
| 464 |
""")
|
| 465 |
|
|
@@ -3,3 +3,4 @@ folium==0.18.0
|
|
| 3 |
requests==2.32.3
|
| 4 |
Pillow==10.4.0
|
| 5 |
beautifulsoup4==4.12.3
|
|
|
|
|
|
| 3 |
requests==2.32.3
|
| 4 |
Pillow==10.4.0
|
| 5 |
beautifulsoup4==4.12.3
|
| 6 |
+
numpy==1.26.4
|