#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Figure 6: global dfs_in and dfs_out at 1/20 degree, and the example basin 8595 at 3 arcsec. Input: ../Data/dfs_in_global_020th_v11.tif, dfs_out_global_020th_v11.tif, rgn_global_200th_v11.gpkg, ex8595_*.tif, ex8595_record.json. Output: ../Figures/Fig06_DFS_Interval_Global_v11.png and .pdf. Run from this folder; ../Data holds the inputs and the figure is written to ../Figures. """ import os import json import numpy as np import rasterio from rasterio.features import shapes as raster_shapes from shapely.geometry import shape as shp_shape from shapely.ops import unary_union from rasterio.enums import Resampling import geopandas as gpd import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.ticker as mticker from matplotlib.gridspec import GridSpec from mpl_toolkits.axes_grid1 import make_axes_locatable # ============================================================================ # Style (ESSD / AGU – Helvetica, cm mathtext, 9 pt) # ============================================================================ mpl.rcParams.update({ "font.family": "Helvetica", "font.size": 9, "axes.labelsize": 9, "axes.titlesize": 10, "axes.titleweight": "bold", "xtick.labelsize": 8, "ytick.labelsize": 8, "mathtext.fontset": "cm", "axes.linewidth": 0.6, }) # ============================================================================ # Paths # ============================================================================ HERE = os.path.dirname(os.path.abspath(__file__)) DATA = os.path.join(HERE, "..", "Data") OUT_DIR = os.path.join(HERE, "..", "Figures") os.makedirs(OUT_DIR, exist_ok=True) G_DFSIN = os.path.join(DATA, "dfs_in_global_020th_v11.tif") G_DFSOUT = os.path.join(DATA, "dfs_out_global_020th_v11.tif") G_RGN = os.path.join(DATA, "rgn_global_200th_v11.gpkg") E_DFSIN = os.path.join(DATA, "ex8595_dfs_in.tif") E_DFSOUT = os.path.join(DATA, "ex8595_dfs_out.tif") E_BSN = os.path.join(DATA, "ex8595_bsn.tif") E_REC = os.path.join(DATA, "ex8595_record.json") FOCUS_BSN_ID = 8595 # global_basin_id (the v1.1 basin raster holds global ids) # ============================================================================ # Region boundaries (drawn on global panels a/c) # ============================================================================ gdf_regions = gpd.read_file(G_RGN) assert len(gdf_regions) == 90, len(gdf_regions) # ============================================================================ # Resolve example-basin geometry (focus mask, outlet, polygon, bbox) # ============================================================================ with rasterio.open(E_BSN) as ds: bsn_arr = ds.read(1) bsn_xform = ds.transform FOCUS_MASK = (bsn_arr == FOCUS_BSN_ID) REC = json.load(open(E_REC)) r_out, c_out = REC["outlet_row_in_window"], REC["outlet_col_in_window"] OUTLET_LON, OUTLET_LAT = rasterio.transform.xy(bsn_xform, r_out, c_out) _polys = [shp_shape(g) for g, v in raster_shapes(FOCUS_MASK.astype(np.uint8), mask=FOCUS_MASK, transform=bsn_xform) if int(v) == 1] gdf_basin = gpd.GeoDataFrame(geometry=[unary_union(_polys)], crs="EPSG:4326") basin_bounds = gdf_basin.total_bounds BLON0, BLAT0, BLON1, BLAT1 = basin_bounds # Red locator rectangle on globe — bigger pad so it reads at 360°×145° PAD_DEG = 4.0 RBOX_LON0 = BLON0 - PAD_DEG; RBOX_LON1 = BLON1 + PAD_DEG RBOX_LAT0 = BLAT0 - PAD_DEG; RBOX_LAT1 = BLAT1 + PAD_DEG # Basin zoom panels b/d: subtract the outlet's dfs_in, the basin's first # number (the outlet is entered first): (b) runs 0 .. n - 1, (d) from the # first source pixel's number up to n - 1; the outlet's interval is [0, n - 1]. with rasterio.open(E_DFSIN) as ds: _din = ds.read(1) DFSIN_OFFSET = int(_din[r_out, c_out]) assert DFSIN_OFFSET == int(_din[FOCUS_MASK].min()) == REC["dfs_in_first"] DFSOUT_OFFSET = DFSIN_OFFSET # ============================================================================ # Helpers # ============================================================================ def human_fmt(v, _): """Compact tick formatter: 1.2M, 350k, 480, etc.""" av = abs(v) if av >= 1e9: x = v / 1e9 return f"{x:.1f}B" if av >= 1e6: x = v / 1e6 return f"{x:.0f}M" if x == int(x) else f"{x:.1f}M" if av >= 1e3: x = v / 1e3 return f"{x:.0f}k" if x == int(x) else f"{x:.1f}k" return f"{v:.0f}" def plot_global(fig, ax, tif_path, cbar_label): with rasterio.open(tif_path) as ds: # Subsample × 2 for plotting speed (already 5 km; 10 km display is fine) data = ds.read(1, out_shape=(ds.height // 2, ds.width // 2), resampling=Resampling.nearest).astype(np.float32) nd = ds.nodata valid = np.isfinite(data) if nd is not None: valid &= (data != nd) data = np.ma.masked_where(~valid, data) ext = (ds.bounds.left, ds.bounds.right, ds.bounds.bottom, ds.bounds.top) vmin = float(data.min()) if valid.any() else 0.0 vmax = float(data.max()) if valid.any() else 1.0 im = ax.imshow(data, extent=ext, origin="upper", cmap="viridis", vmin=vmin, vmax=vmax, interpolation="nearest") # Region boundaries: thin white lines so the per-region DFS index # segmentation is explicit on top of the viridis raster. gdf_regions.boundary.plot(ax=ax, color="white", linewidth=0.3, alpha=0.8, zorder=4) div = make_axes_locatable(ax) cax = div.append_axes("right", size="2.2%", pad=0.08) cb = fig.colorbar(im, cax=cax) cb.ax.yaxis.set_major_formatter(mticker.FuncFormatter(human_fmt)) cb.ax.tick_params(labelsize=7, length=2) cb.outline.set_linewidth(0.4) rect = plt.Rectangle((RBOX_LON0, RBOX_LAT0), RBOX_LON1 - RBOX_LON0, RBOX_LAT1 - RBOX_LAT0, lw=1.8, edgecolor="red", facecolor="none", zorder=5) ax.add_patch(rect) ax.set_xlim(-180, 180); ax.set_ylim(-60, 85) ax.set_xticks([-180, -90, 0, 90, 180]) ax.set_yticks([-60, -30, 0, 30, 60]) ax.set_xlabel("Longitude [°]") ax.set_ylabel("Latitude [°]") for s in ("top", "right"): ax.spines[s].set_visible(False) def plot_basin(fig, ax, tif_path, cbar_label, offset=0): with rasterio.open(tif_path) as ds: data = ds.read(1).astype(np.float64) nd = ds.nodata valid = np.isfinite(data) & FOCUS_MASK if nd is not None: valid &= (data != nd) data = data - offset # local-relative values data = np.ma.masked_where(~valid, data) ext = (ds.bounds.left, ds.bounds.right, ds.bounds.bottom, ds.bounds.top) vmin = float(data.min()) if valid.any() else 0.0 vmax = float(data.max()) if valid.any() else 1.0 im = ax.imshow(data, extent=ext, origin="upper", cmap="viridis", vmin=vmin, vmax=vmax, interpolation="nearest") gdf_basin.boundary.plot(ax=ax, color="black", linewidth=0.8, zorder=5) ax.plot(OUTLET_LON, OUTLET_LAT, marker="v", markersize=7, markerfacecolor="#E53935", markeredgecolor="black", markeredgewidth=0.7, linestyle="", zorder=10) div = make_axes_locatable(ax) cax = div.append_axes("right", size="3%", pad=0.08) cb = fig.colorbar(im, cax=cax) cb.ax.yaxis.set_major_formatter(mticker.FuncFormatter(human_fmt)) cb.ax.tick_params(labelsize=7, length=2) cb.outline.set_linewidth(0.4) cb.set_label(cbar_label, fontsize=7.5, labelpad=3) xmin, ymin, xmax, ymax = basin_bounds px = (xmax - xmin) * 0.04; py = (ymax - ymin) * 0.04 ax.set_xlim(xmin - px, xmax + px) ax.set_ylim(ymin - py, ymax + py) ax.set_xticks([]); ax.set_yticks([]) for s in ax.spines.values(): s.set_visible(False) # ============================================================================ # Main # ============================================================================ def main(): fig = plt.figure(figsize=(7.4, 4.4)) gs = GridSpec(2, 2, figure=fig, width_ratios=[1.65, 1], hspace=0.32, wspace=0.16) panels = [ (G_DFSIN, E_DFSIN, r"(a) Global $\mathit{dfs\_in}$", r"(b) Example basin, $\mathit{dfs\_in}$", r"$\mathit{dfs\_in}$", r"$\mathit{dfs\_in} - \mathit{dfs\_in}_{\mathrm{outlet}}$", DFSIN_OFFSET), (G_DFSOUT, E_DFSOUT, r"(c) Global $\mathit{dfs\_out}$", r"(d) Example basin, $\mathit{dfs\_out}$", r"$\mathit{dfs\_out}$", r"$\mathit{dfs\_out} - \mathit{dfs\_in}_{\mathrm{outlet}}$", DFSOUT_OFFSET), ] for i, (g_tif, e_tif, t_l, t_r, cb_g, cb_b, off) in enumerate(panels): ax_l = fig.add_subplot(gs[i, 0]) ax_r = fig.add_subplot(gs[i, 1]) plot_global(fig, ax_l, g_tif, cb_g) ax_l.set_title(t_l, loc="left", pad=4) plot_basin(fig, ax_r, e_tif, cb_b, offset=off) ax_r.set_title(t_r, loc="left", pad=4) fig.subplots_adjust(left=0.06, right=0.96, top=0.94, bottom=0.07) for ext in ("png", "pdf"): out = os.path.join(OUT_DIR, f"Fig06_DFS_Interval_Global_v11.{ext}") if os.path.exists(out): raise SystemExit(f"{out} is there already") fig.savefig(out, dpi=300, bbox_inches="tight", facecolor="white") print(f"Saved: {out}") print(f" Outlet: lon={OUTLET_LON:.4f}°, lat={OUTLET_LAT:.4f}°") print(f" Basin bounds (lon0,lat0,lon1,lat1) = " f"({BLON0:.3f}, {BLAT0:.3f}, {BLON1:.3f}, {BLAT1:.3f})") print(f" Focus pixels (global_basin_id={FOCUS_BSN_ID}): {int(FOCUS_MASK.sum())}") plt.close(fig) if __name__ == "__main__": main()