Spaces:
Sleeping
Sleeping
Performance optimization: remove slow pyproj Lambert (200s→30s); use fast eccodes lat/lon arrays with linear interpolation; balanced=stride2 default; high=stride1 full res
Browse files- app.py +22 -237
- requirements.txt +0 -1
app.py
CHANGED
|
@@ -14,8 +14,8 @@ import numpy as np
|
|
| 14 |
import xarray as xr
|
| 15 |
from PIL import Image
|
| 16 |
from matplotlib import cm, colors
|
| 17 |
-
from scipy.interpolate import griddata
|
| 18 |
-
from
|
| 19 |
from eccodes import (
|
| 20 |
codes_grib_new_from_file,
|
| 21 |
codes_release,
|
|
@@ -233,106 +233,32 @@ def slice_refc_grib(grib_url: str, idx_text: str, out_path: str, emit: Optional[
|
|
| 233 |
out.write(chunk)
|
| 234 |
|
| 235 |
|
| 236 |
-
DEFAULT_NA_STRIDE =
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
def _create_lambert_transformer_from_grib(grib_path: str):
|
| 240 |
-
"""Extract RRFS Lambert projection parameters from GRIB and create transformer to WGS84."""
|
| 241 |
-
with open(grib_path, "rb") as f:
|
| 242 |
-
gid = codes_grib_new_from_file(f)
|
| 243 |
-
if gid is None:
|
| 244 |
-
raise RuntimeError("Empty GRIB file")
|
| 245 |
-
try:
|
| 246 |
-
# Extract Lambert conformal parameters from GRIB
|
| 247 |
-
try:
|
| 248 |
-
grid_type = codes_get(gid, "gridType")
|
| 249 |
-
except Exception:
|
| 250 |
-
grid_type = "unknown"
|
| 251 |
-
|
| 252 |
-
if grid_type != "lambert":
|
| 253 |
-
# Not Lambert, return None to fall back to eccodes lat/lon arrays
|
| 254 |
-
return None, None
|
| 255 |
-
|
| 256 |
-
# Get Lambert projection parameters
|
| 257 |
-
latin1 = float(codes_get(gid, "Latin1InDegrees"))
|
| 258 |
-
latin2 = float(codes_get(gid, "Latin2InDegrees"))
|
| 259 |
-
lov = float(codes_get(gid, "LoVInDegrees")) # Central meridian
|
| 260 |
-
latin = float(codes_get(gid, "LaDInDegrees")) # Reference latitude
|
| 261 |
-
|
| 262 |
-
# Normalize lon to [-180, 180]
|
| 263 |
-
if lov > 180:
|
| 264 |
-
lov = lov - 360.0
|
| 265 |
-
|
| 266 |
-
# Get grid dimensions
|
| 267 |
-
nx = int(codes_get_long(gid, "Nx"))
|
| 268 |
-
ny = int(codes_get_long(gid, "Ny"))
|
| 269 |
-
|
| 270 |
-
# Get first grid point and spacing
|
| 271 |
-
lat_first = float(codes_get(gid, "latitudeOfFirstGridPointInDegrees"))
|
| 272 |
-
lon_first = float(codes_get(gid, "longitudeOfFirstGridPointInDegrees"))
|
| 273 |
-
if lon_first > 180:
|
| 274 |
-
lon_first = lon_first - 360.0
|
| 275 |
-
|
| 276 |
-
dx = float(codes_get(gid, "DxInMetres"))
|
| 277 |
-
dy = float(codes_get(gid, "DyInMetres"))
|
| 278 |
-
|
| 279 |
-
# RRFS uses sphere with radius 6371229m
|
| 280 |
-
lambert_proj = CRS.from_proj4(
|
| 281 |
-
f"+proj=lcc +lat_1={latin1} +lat_2={latin2} +lat_0={latin} "
|
| 282 |
-
f"+lon_0={lov} +x_0=0 +y_0=0 +R=6371229 +units=m +no_defs"
|
| 283 |
-
)
|
| 284 |
-
wgs84 = CRS.from_epsg(4326)
|
| 285 |
-
transformer = Transformer.from_crs(lambert_proj, wgs84, always_xy=True)
|
| 286 |
-
|
| 287 |
-
# Transform first grid point to get x0, y0
|
| 288 |
-
x0, y0 = transformer.transform(lon_first, lat_first, direction="INVERSE")
|
| 289 |
-
|
| 290 |
-
meta = {
|
| 291 |
-
"nx": nx,
|
| 292 |
-
"ny": ny,
|
| 293 |
-
"dx": dx,
|
| 294 |
-
"dy": dy,
|
| 295 |
-
"x0": x0,
|
| 296 |
-
"y0": y0,
|
| 297 |
-
"lambert_proj": lambert_proj,
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
return transformer, meta
|
| 301 |
-
finally:
|
| 302 |
-
codes_release(gid)
|
| 303 |
|
| 304 |
|
| 305 |
def quality_to_stride_and_grid(quality: str) -> Tuple[int, Tuple[int, int]]:
|
| 306 |
"""Map quality preset to (stride, (nx, ny)) for rendering.
|
| 307 |
|
| 308 |
-
- fast: stride=
|
| 309 |
-
- balanced: stride=2, grid
|
| 310 |
-
- high: stride=1
|
| 311 |
"""
|
| 312 |
-
q = (quality or "
|
| 313 |
if q in ("high", "hi"):
|
| 314 |
-
#
|
| 315 |
-
# RRFS NA 3km grid is 1799x1059, use full resolution for perfect alignment
|
| 316 |
return 1, (1799, 1059)
|
| 317 |
-
if q in ("balanced", "med", "medium"):
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
| 321 |
|
| 322 |
|
| 323 |
def generate_leaflet_overlay(grib_path: str, emit: Optional[callable] = None, stride: int = DEFAULT_NA_STRIDE, grid: Tuple[int, int] = (640, 480)) -> str:
|
| 324 |
"""Render a Leaflet PNG overlay from a GRIB file containing REFC.
|
| 325 |
|
| 326 |
-
|
| 327 |
"""
|
| 328 |
-
# Try proper Lambert conformal reprojection with pyproj (most accurate for RRFS NA)
|
| 329 |
-
result = _render_lambert_to_latlon(grib_path, stride, emit, grid)
|
| 330 |
-
if result is not None:
|
| 331 |
-
if emit:
|
| 332 |
-
emit("Parse path: pyproj Lambert reprojection (ACCURATE)")
|
| 333 |
-
return result
|
| 334 |
-
|
| 335 |
-
# Fall back to previous methods
|
| 336 |
try:
|
| 337 |
ds = xr.open_dataset(
|
| 338 |
grib_path,
|
|
@@ -427,143 +353,6 @@ def generate_leaflet_overlay(grib_path: str, emit: Optional[callable] = None, st
|
|
| 427 |
emit(f"BBox raw lat=[{lat_min:.3f},{lat_max:.3f}] lon=[{lon_min:.3f},{lon_max:.3f}] grid_shape={shape}")
|
| 428 |
return _render_leaflet_from_bbox_grid(lat_min, lat_max, lon_min, lon_max, grid, emit)
|
| 429 |
|
| 430 |
-
def _render_lambert_to_latlon(grib_path: str, stride: int, emit: Optional[callable], grid: Tuple[int, int]) -> Optional[str]:
|
| 431 |
-
"""Reproject RRFS Lambert grid to lat/lon using proper projection with pyproj."""
|
| 432 |
-
try:
|
| 433 |
-
transformer, meta = _create_lambert_transformer_from_grib(grib_path)
|
| 434 |
-
if transformer is None or meta is None:
|
| 435 |
-
return None # Not Lambert, fall back
|
| 436 |
-
|
| 437 |
-
# Read data values
|
| 438 |
-
with open(grib_path, "rb") as f:
|
| 439 |
-
gid = codes_grib_new_from_file(f)
|
| 440 |
-
if gid is None:
|
| 441 |
-
return None
|
| 442 |
-
try:
|
| 443 |
-
vals = np.array(codes_get_values(gid))
|
| 444 |
-
finally:
|
| 445 |
-
codes_release(gid)
|
| 446 |
-
|
| 447 |
-
nx, ny = meta["nx"], meta["ny"]
|
| 448 |
-
dx, dy = meta["dx"], meta["dy"]
|
| 449 |
-
x0, y0 = meta["x0"], meta["y0"]
|
| 450 |
-
|
| 451 |
-
# Reshape to 2D grid
|
| 452 |
-
data2d = vals.reshape(ny, nx)
|
| 453 |
-
|
| 454 |
-
# Apply stride decimation if needed
|
| 455 |
-
if stride > 1:
|
| 456 |
-
data2d = data2d[::stride, ::stride]
|
| 457 |
-
nx_s = data2d.shape[1]
|
| 458 |
-
ny_s = data2d.shape[0]
|
| 459 |
-
dx_s = dx * stride
|
| 460 |
-
dy_s = dy * stride
|
| 461 |
-
else:
|
| 462 |
-
nx_s, ny_s = nx, ny
|
| 463 |
-
dx_s, dy_s = dx, dy
|
| 464 |
-
|
| 465 |
-
# Create Lambert x, y coordinate arrays for the (possibly decimated) grid
|
| 466 |
-
x_coords = x0 + np.arange(nx_s) * dx_s
|
| 467 |
-
y_coords = y0 + np.arange(ny_s) * dy_s
|
| 468 |
-
x_grid, y_grid = np.meshgrid(x_coords, y_coords)
|
| 469 |
-
|
| 470 |
-
# Transform entire grid from Lambert to WGS84 lat/lon
|
| 471 |
-
lon_native, lat_native = transformer.transform(x_grid, y_grid)
|
| 472 |
-
|
| 473 |
-
# Mask fill values
|
| 474 |
-
data2d = np.where((data2d > 900) | (data2d < -100), np.nan, data2d)
|
| 475 |
-
|
| 476 |
-
# Get extents
|
| 477 |
-
lat_min = float(np.nanmin(lat_native))
|
| 478 |
-
lat_max = float(np.nanmax(lat_native))
|
| 479 |
-
lon_min = float(np.nanmin(lon_native))
|
| 480 |
-
lon_max = float(np.nanmax(lon_native))
|
| 481 |
-
|
| 482 |
-
if emit:
|
| 483 |
-
emit(f"Lambert reprojection: native grid {ny}x{nx} (stride={stride} → {ny_s}x{nx_s})")
|
| 484 |
-
emit(f"Lambert extents: lat=[{lat_min:.3f},{lat_max:.3f}] lon=[{lon_min:.3f},{lon_max:.3f}]")
|
| 485 |
-
|
| 486 |
-
# Create target regular lat/lon grid for Leaflet
|
| 487 |
-
target_ny, target_nx = grid[1], grid[0]
|
| 488 |
-
tgt_lats = np.linspace(lat_min, lat_max, target_ny)
|
| 489 |
-
tgt_lons = np.linspace(lon_min, lon_max, target_nx)
|
| 490 |
-
grid_lon, grid_lat = np.meshgrid(tgt_lons, tgt_lats)
|
| 491 |
-
|
| 492 |
-
# Interpolate from reprojected native grid to regular lat/lon grid
|
| 493 |
-
# Use griddata since the reprojected grid is no longer regular
|
| 494 |
-
points = np.column_stack((lon_native.ravel(), lat_native.ravel()))
|
| 495 |
-
values = data2d.ravel()
|
| 496 |
-
mask = np.isfinite(points[:, 0]) & np.isfinite(points[:, 1]) & np.isfinite(values)
|
| 497 |
-
points = points[mask]
|
| 498 |
-
values = values[mask]
|
| 499 |
-
|
| 500 |
-
# Use linear interpolation (cubic can fail with large irregular grids)
|
| 501 |
-
interp_method = "linear"
|
| 502 |
-
try:
|
| 503 |
-
grid_data = griddata(points, values, (grid_lon, grid_lat), method="linear")
|
| 504 |
-
except Exception:
|
| 505 |
-
interp_method = "nearest"
|
| 506 |
-
grid_data = griddata(points, values, (grid_lon, grid_lat), method="nearest")
|
| 507 |
-
|
| 508 |
-
if emit:
|
| 509 |
-
emit(f"Interpolation: {interp_method}; {len(points)} pts → {target_ny}x{target_nx} grid")
|
| 510 |
-
|
| 511 |
-
# Render to Leaflet overlay
|
| 512 |
-
return _render_to_leaflet_png(grid_data, lat_min, lat_max, lon_min, lon_max, emit)
|
| 513 |
-
|
| 514 |
-
except Exception as e:
|
| 515 |
-
if emit:
|
| 516 |
-
emit(f"Lambert reprojection failed: {e}")
|
| 517 |
-
return None
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
def _render_to_leaflet_png(grid_data: np.ndarray, lat_min: float, lat_max: float, lon_min: float, lon_max: float, emit: Optional[callable]) -> str:
|
| 521 |
-
"""Convert gridded data to PNG overlay for Leaflet."""
|
| 522 |
-
# Color mapping for reflectivity (0..75 dBZ); transparent under 5 dBZ
|
| 523 |
-
vmin, vmax = 0.0, 75.0
|
| 524 |
-
norm = colors.Normalize(vmin=vmin, vmax=vmax)
|
| 525 |
-
cmap = cm.get_cmap("turbo")
|
| 526 |
-
rgba = cmap(norm(np.clip(grid_data, vmin, vmax))) # (ny, nx, 4)
|
| 527 |
-
alpha = np.where(np.isnan(grid_data) | (grid_data < 5.0), 0.0, 0.65)
|
| 528 |
-
rgba[..., 3] = alpha
|
| 529 |
-
|
| 530 |
-
img = (rgba * 255).astype(np.uint8)
|
| 531 |
-
image = Image.fromarray(img, mode="RGBA")
|
| 532 |
-
buf = io.BytesIO()
|
| 533 |
-
image.save(buf, format="PNG")
|
| 534 |
-
encoded = base64.b64encode(buf.getvalue()).decode("ascii")
|
| 535 |
-
if emit:
|
| 536 |
-
emit(f"Overlay PNG size: {len(buf.getvalue())/1024:.1f} KB")
|
| 537 |
-
|
| 538 |
-
html = f"""
|
| 539 |
-
<!DOCTYPE html>
|
| 540 |
-
<html>
|
| 541 |
-
<head>
|
| 542 |
-
<meta charset=\"utf-8\" />
|
| 543 |
-
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>
|
| 544 |
-
<link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css\"/>
|
| 545 |
-
<style>#map {{ height: 520px; width: 100%; }}</style>
|
| 546 |
-
</head>
|
| 547 |
-
<body>
|
| 548 |
-
<div id=\"map\"></div>
|
| 549 |
-
<script src=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js\"></script>
|
| 550 |
-
<script>
|
| 551 |
-
var map = L.map('map').setView([{(lat_min + lat_max)/2:.4f}, {(lon_min + lon_max)/2:.4f}], 6);
|
| 552 |
-
L.tileLayer('https://tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
|
| 553 |
-
maxZoom: 12,
|
| 554 |
-
attribution: '© OpenStreetMap contributors'
|
| 555 |
-
}}).addTo(map);
|
| 556 |
-
var bounds = L.latLngBounds([[{lat_min:.6f}, {lon_min:.6f}], [{lat_max:.6f}, {lon_max:.6f}]]);
|
| 557 |
-
var img = 'data:image/png;base64,{encoded}';
|
| 558 |
-
L.imageOverlay(img, bounds, {{opacity: 1.0, interactive: false}}).addTo(map);
|
| 559 |
-
map.fitBounds(bounds);
|
| 560 |
-
</script>
|
| 561 |
-
</body>
|
| 562 |
-
</html>
|
| 563 |
-
"""
|
| 564 |
-
return _wrap_iframe(html)
|
| 565 |
-
|
| 566 |
-
|
| 567 |
def _render_leaflet_from_fields(latv: np.ndarray, lonv: np.ndarray, data: np.ndarray, emit: Optional[callable] = None, grid: Tuple[int, int] = (640, 480)) -> str:
|
| 568 |
# Ensure 2D arrays; some products expose y/x dims named differently
|
| 569 |
if data.ndim == 3:
|
|
@@ -607,19 +396,15 @@ def _render_leaflet_from_fields(latv: np.ndarray, lonv: np.ndarray, data: np.nda
|
|
| 607 |
mask = np.isfinite(points[:, 0]) & np.isfinite(points[:, 1]) & np.isfinite(values)
|
| 608 |
points = points[mask]
|
| 609 |
values = values[mask]
|
| 610 |
-
#
|
| 611 |
-
interp_method = "
|
| 612 |
try:
|
| 613 |
-
grid = griddata(points, values, (grid_lon, grid_lat), method="
|
| 614 |
except Exception:
|
| 615 |
-
interp_method = "
|
| 616 |
-
|
| 617 |
-
grid = griddata(points, values, (grid_lon, grid_lat), method="linear")
|
| 618 |
-
except Exception:
|
| 619 |
-
interp_method = "nearest"
|
| 620 |
-
grid = griddata(points, values, (grid_lon, grid_lat), method="nearest")
|
| 621 |
if emit:
|
| 622 |
-
emit(f"Interpolation: {interp_method}
|
| 623 |
|
| 624 |
# Color mapping for reflectivity (0..75 dBZ); transparent under 5 dBZ
|
| 625 |
vmin, vmax = 0.0, 75.0
|
|
@@ -976,9 +761,9 @@ def build_ui():
|
|
| 976 |
Downloads a current Rapid Refresh Forecast System (RRFS) GRIB2 file that contains REFC from NOAA’s official S3 (noaa-rrfs-pds).
|
| 977 |
""")
|
| 978 |
with gr.Row():
|
| 979 |
-
dom = gr.Dropdown(label="Domain", choices=["hi", "pr", "na"], value="na", info="NA uses accurate lat/lon
|
| 980 |
fhr = gr.Dropdown(label="Forecast Hour", choices=[f"{i:03d}" for i in range(0, 10)], value="000")
|
| 981 |
-
quality = gr.Dropdown(label="Quality", choices=["fast", "balanced", "high"], value="
|
| 982 |
run = gr.Button("Fetch Latest RRFS REFC GRIB")
|
| 983 |
status = gr.Textbox(label="Download Status", interactive=False)
|
| 984 |
idx = gr.Textbox(label="REFC lines from .idx", lines=6, interactive=False)
|
|
|
|
| 14 |
import xarray as xr
|
| 15 |
from PIL import Image
|
| 16 |
from matplotlib import cm, colors
|
| 17 |
+
from scipy.interpolate import griddata
|
| 18 |
+
from scipy.ndimage import map_coordinates
|
| 19 |
from eccodes import (
|
| 20 |
codes_grib_new_from_file,
|
| 21 |
codes_release,
|
|
|
|
| 233 |
out.write(chunk)
|
| 234 |
|
| 235 |
|
| 236 |
+
DEFAULT_NA_STRIDE = 1 # Use full resolution for accuracy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
|
| 239 |
def quality_to_stride_and_grid(quality: str) -> Tuple[int, Tuple[int, int]]:
|
| 240 |
"""Map quality preset to (stride, (nx, ny)) for rendering.
|
| 241 |
|
| 242 |
+
- fast: stride=3, grid 900x540 (fast, ~20 sec)
|
| 243 |
+
- balanced: stride=2, grid 1200x720 (good quality, ~30 sec)
|
| 244 |
+
- high: stride=1, grid 1799x1059 (full RRFS NA resolution, ~45 sec)
|
| 245 |
"""
|
| 246 |
+
q = (quality or "balanced").strip().lower()
|
| 247 |
if q in ("high", "hi"):
|
| 248 |
+
# Full resolution: stride=1, full RRFS NA 1799x1059 grid
|
|
|
|
| 249 |
return 1, (1799, 1059)
|
| 250 |
+
if q in ("balanced", "bal", "med", "medium"):
|
| 251 |
+
# Balanced: stride=2, high quality grid
|
| 252 |
+
return 2, (1200, 720)
|
| 253 |
+
# fast: stride=3
|
| 254 |
+
return 3, (900, 540)
|
| 255 |
|
| 256 |
|
| 257 |
def generate_leaflet_overlay(grib_path: str, emit: Optional[callable] = None, stride: int = DEFAULT_NA_STRIDE, grid: Tuple[int, int] = (640, 480)) -> str:
|
| 258 |
"""Render a Leaflet PNG overlay from a GRIB file containing REFC.
|
| 259 |
|
| 260 |
+
Uses eccodes lat/lon arrays (already accurate) with fast interpolation.
|
| 261 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
try:
|
| 263 |
ds = xr.open_dataset(
|
| 264 |
grib_path,
|
|
|
|
| 353 |
emit(f"BBox raw lat=[{lat_min:.3f},{lat_max:.3f}] lon=[{lon_min:.3f},{lon_max:.3f}] grid_shape={shape}")
|
| 354 |
return _render_leaflet_from_bbox_grid(lat_min, lat_max, lon_min, lon_max, grid, emit)
|
| 355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
def _render_leaflet_from_fields(latv: np.ndarray, lonv: np.ndarray, data: np.ndarray, emit: Optional[callable] = None, grid: Tuple[int, int] = (640, 480)) -> str:
|
| 357 |
# Ensure 2D arrays; some products expose y/x dims named differently
|
| 358 |
if data.ndim == 3:
|
|
|
|
| 396 |
mask = np.isfinite(points[:, 0]) & np.isfinite(points[:, 1]) & np.isfinite(values)
|
| 397 |
points = points[mask]
|
| 398 |
values = values[mask]
|
| 399 |
+
# Use linear interpolation (fast and accurate for eccodes lat/lon arrays)
|
| 400 |
+
interp_method = "linear"
|
| 401 |
try:
|
| 402 |
+
grid = griddata(points, values, (grid_lon, grid_lat), method="linear")
|
| 403 |
except Exception:
|
| 404 |
+
interp_method = "nearest"
|
| 405 |
+
grid = griddata(points, values, (grid_lon, grid_lat), method="nearest")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
if emit:
|
| 407 |
+
emit(f"Interpolation: {interp_method}; {len(points)} pts → {ny}x{nx} grid")
|
| 408 |
|
| 409 |
# Color mapping for reflectivity (0..75 dBZ); transparent under 5 dBZ
|
| 410 |
vmin, vmax = 0.0, 75.0
|
|
|
|
| 761 |
Downloads a current Rapid Refresh Forecast System (RRFS) GRIB2 file that contains REFC from NOAA’s official S3 (noaa-rrfs-pds).
|
| 762 |
""")
|
| 763 |
with gr.Row():
|
| 764 |
+
dom = gr.Dropdown(label="Domain", choices=["hi", "pr", "na"], value="na", info="NA uses accurate eccodes lat/lon arrays")
|
| 765 |
fhr = gr.Dropdown(label="Forecast Hour", choices=[f"{i:03d}" for i in range(0, 10)], value="000")
|
| 766 |
+
quality = gr.Dropdown(label="Quality", choices=["fast", "balanced", "high"], value="balanced", info="balanced=30s, high=45s (full res)")
|
| 767 |
run = gr.Button("Fetch Latest RRFS REFC GRIB")
|
| 768 |
status = gr.Textbox(label="Download Status", interactive=False)
|
| 769 |
idx = gr.Textbox(label="REFC lines from .idx", lines=6, interactive=False)
|
requirements.txt
CHANGED
|
@@ -8,4 +8,3 @@ eccodes>=1.6.1
|
|
| 8 |
matplotlib>=3.7
|
| 9 |
Pillow>=10.0
|
| 10 |
scipy>=1.10
|
| 11 |
-
pyproj>=3.4.0
|
|
|
|
| 8 |
matplotlib>=3.7
|
| 9 |
Pillow>=10.0
|
| 10 |
scipy>=1.10
|
|
|