← LAMPIRAN MENULAMPIRAN 18 / 18
S1D4NG.5KR1P51.404 · ANNEX FILE
Lampiran 18. Program Python Plot HYSPLIT
//⚠ S1D4NG.5KR1P51.404 — BIOHAZARD ZONE·//⚠ NO SHARP · NO NARCOTICS · NO WEIRD CARGO·//⚠ THE HUM IS LOUDER WHEN YOU NOTICE IT·//⚠ ERROR 404 RECOVERED — STILL WRONG·//⚠ LEVEL 0 · DO NOT TRUST THE CORRIDOR·
Lampiran 18. Program Python Plot HYSPLIT
| #!/usr/bin/env python3 import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.patches as mpatches from matplotlib.lines import Line2D import cartopy.crs as ccrs import cartopy.feature as cfeature from pathlib import Path import numpy as np OCEAN_COLORS = ['#c6d9f1', '#adc8e3', '#8eb4d9', '#6699cc'] # gradient LAND_COLOR = '#ffffff' COAST_COLOR = '#000000' BORDER_COLOR = '#666666' GRID_COLOR = '#434343' TRAJ_COLORS = { 1: '#ff0000', # RED — 925 hPa / 800m 2: '#0000ff', # BLUE — 850 hPa / 1500m 3: '#00cc00', # GREEN — 700 hPa / 3000m 4: '#cc00cc', # MAGENTA — 500 hPa / 5500m } LEVEL_INFO = { 1: {'color': '#ff0000', 'hpa': '925', 'hgt': '800', 'label': '925 hPa (800 m)'}, 2: {'color': '#0000ff', 'hpa': '850', 'hgt': '1500', 'label': '850 hPa (1500 m)'}, 3: {'color': '#00cc00', 'hpa': '700', 'hgt': '3000', 'label': '700 hPa (3000 m)'}, 4: {'color': '#cc00cc', 'hpa': '500', 'hgt': '5500', 'label': '500 hPa (5500 m)'}, } START_LAT, START_LON = -6.79, 110.87 INPUT_DIR = Path(r"~\Skripsi\output\hysplit_4level") OUTPUT_DIR = Path(r"~\Skripsi\output\hysplit_4level\plots_ready_final") OUTPUT_DIR.mkdir(parents=True, exist_ok=True) def parse_tdump(filepath): trajectories = {} with open(filepath) as f: lines = f.readlines() n_levels = None direction_line = None for i, line in enumerate(lines): if 'BACKWARD' in line or 'FORWARD' in line: n_levels = int(line.split()[0]) direction_line = i break if n_levels is None: return trajectories, 0 data_start = direction_line + n_levels + 2 for line in lines[data_start:]: parts = line.split() if len(parts) >= 13: try: traj_num = int(parts[0]) lat = float(parts[9]) lon = float(parts[10]) height = float(parts[11]) pressure = float(parts[12]) age = float(parts[8]) if traj_num not in trajectories: trajectories[traj_num] = [] trajectories[traj_num].append((lat, lon, height, pressure, age)) except (ValueError, IndexError): continue return trajectories, n_levels def auto_extent(trajs, padding=1.5): all_lats, all_lons = [START_LAT], [START_LON] for points in trajs.values(): for p in points: all_lats.append(p[0]) all_lons.append(p[1]) return [ min(all_lons) - padding, max(all_lons) + padding, min(all_lats) - padding, max(all_lats) + padding ] def draw_ocean_gradient(ax, extent): from matplotlib.colors import LinearSegmentedColormap cmap = LinearSegmentedColormap.from_list('ocean', OCEAN_COLORS[::-1]) # Create gradient effect x = np.linspace(extent[0], extent[1], 100) y = np.linspace(extent[2], extent[3], 100) xx, yy = np.meshgrid(x, y) gradient = np.linspace(0, 1, 100).reshape(-1, 1) gradient = np.tile(gradient, (1, 100)) ax.pcolormesh(xx, yy, gradient, cmap=cmap, alpha=0.4, transform=ccrs.PlateCarree(), zorder=0) def draw_mini_trajectory(ax, traj_points, color, x_offset, y_center, scale_x, scale_y): if not traj_points: return # Normalize trajectory to fit in mini area lats = [p[0] for p in traj_points] lons = [p[1] for p in traj_points] min_lon, max_lon = min(lons), max(lons) min_lat, max_lat = min(lats), max(lats) lon_range = max(max_lon - min_lon, 0.01) lat_range = max(max_lat - min_lat, 0.01) for i in range(len(lons) - 1): x1 = x_offset + (lons[i] - min_lon) / lon_range * scale_x x2 = x_offset + (lons[i+1] - min_lon) / lon_range * scale_x y1 = y_center + (lats[i] - min_lat) / lat_range * scale_y - scale_y/2 y2 = y_center + (lats[i+1] - min_lat) / lat_range * scale_y - scale_y/2 ax.plot([x1, x2], [y1, y2], color=color, linewidth=1.5, solid_capstyle='round', transform=ax.transAxes) mid = len(lons) // 2 if mid < len(lons) - 1: x1 = x_offset + (lons[mid] - min_lon) / lon_range * scale_x x2 = x_offset + (lons[mid+1] - min_lon) / lon_range * scale_x y1 = y_center + (lats[mid] - min_lat) / lat_range * scale_y - scale_y/2 y2 = y_center + (lats[mid+1] - min_lat) / lat_range * scale_y - scale_y/2 ax.annotate('', xy=(x2, y2), xytext=(x1, y1), arrowprops=dict(arrowstyle='->', color=color, lw=1.0), transform=ax.transAxes) x_start = x_offset + 0 y_start = y_center - scale_y/2 ax.plot(x_start, y_start, 'o', color=color, markersize=4, transform=ax.transAxes, zorder=5) x_end = x_offset + scale_x y_end = y_center + scale_y/2 ax.plot(x_end, y_end, 'o', color=color, markersize=3, transform=ax.transAxes, zorder=5) def plot_ready_web(tdump_path, outpath): trajs, n_levels = parse_tdump(tdump_path) if not trajs: print(f" SKIP: {tdump_path.name}") return extent = auto_extent(trajs) fig = plt.figure(figsize=(5.88, 7.30), dpi=100, facecolor='white') ax_title = fig.add_axes([0.06, 0.88, 0.88, 0.10]) ax_title.set_xlim(0, 1) ax_title.set_ylim(0, 1) ax_title.axis('off') ax_map = fig.add_axes([0.06, 0.18, 0.88, 0.68], projection=ccrs.PlateCarree()) ax_legend = fig.add_axes([0.06, 0.06, 0.88, 0.12]) ax_legend.set_xlim(0, 1) ax_legend.set_ylim(0, 1) ax_legend.axis('off') ax_info = fig.add_axes([0.06, 0.00, 0.88, 0.06]) ax_info.set_xlim(0, 1) ax_info.set_ylim(0, 1) ax_info.axis('off') fname = tdump_path.stem parts = fname.split('_') date_obj = f"{parts[1][4:6]}/{parts[1][6:8]}/20{parts[1][0:2]}" hour_str = parts[2][:2] ax_title.text(0.5, 0.92, 'NOAA HYSPLIT MODEL', fontsize=11, fontweight='bold', ha='center', fontfamily='monospace', color='#333333') ax_title.text(0.5, 0.58, f'Backward trajectories ending {date_obj} {hour_str}:00 UTC', fontsize=8.5, ha='center', fontfamily='monospace', color='#555555') ax_title.text(0.5, 0.22, f'Start: {abs(START_LAT):.2f}°S {START_LON:.2f}°E | GDAS 1° | 24h', fontsize=7.5, ha='center', fontfamily='monospace', color='#777777') ax_title.axhline(y=0.0, xmin=0.0, xmax=1.0, color='#cccccc', linewidth=1.0) ax_map.set_extent(extent, crs=ccrs.PlateCarree()) # Ocean with gradient draw_ocean_gradient(ax_map, extent) ax_map.add_feature(cfeature.OCEAN, facecolor=OCEAN_COLORS[0], alpha=0.3, zorder=0) ax_map.add_feature(cfeature.LAND, facecolor=LAND_COLOR, zorder=1) ax_map.add_feature(cfeature.COASTLINE, linewidth=0.7, color=COAST_COLOR, zorder=3) ax_map.add_feature(cfeature.BORDERS, linestyle='--', linewidth=0.3, color=BORDER_COLOR, zorder=3) # Gridlines gl = ax_map.gridlines(linewidth=0.25, color=GRID_COLOR, alpha=0.5, draw_labels=True, linestyle='-', zorder=2) gl.top_labels = False gl.right_labels = False for traj_num, points in trajs.items(): if traj_num > n_levels: continue info = LEVEL_INFO.get(traj_num) if not info: continue color = info['color'] lats = [p[0] for p in points] lons = [p[1] for p in points] ax_map.plot(lons, lats, color=color, linewidth=2.0, alpha=1.0, solid_capstyle='round', solid_joinstyle='round', transform=ccrs.PlateCarree(), zorder=5) # Arrow markers n_pts = len(lons) arrow_interval = max(1, n_pts // 5) for j in range(0, n_pts - 1, arrow_interval): ax_map.annotate('', xy=(lons[j + 1], lats[j + 1]), xytext=(lons[j], lats[j]), arrowprops=dict(arrowstyle='->', color=color, lw=1.5, mutation_scale=12), transform=ccrs.PlateCarree(), zorder=6) ax_map.plot(lons[-1], lats[-1], 'o', color=color, markersize=3.5, markeredgecolor='none', transform=ccrs.PlateCarree(), zorder=7) ax_map.plot(START_LON, START_LAT, 'o', color='#ff0000', markersize=9, markeredgecolor='black', markeredgewidth=1.5, transform=ccrs.PlateCarree(), zorder=10) ax_legend.axhline(y=0.98, xmin=0.0, xmax=1.0, color='#cccccc', linewidth=1.0) n_legend = min(n_levels, 4) row_height = 0.90 / n_legend for i in range(1, n_legend + 1): info = LEVEL_INFO.get(i) if not info: continue color = info['color'] y_center = 0.95 - (i - 0.5) * row_height traj_points = trajs.get(i, []) if traj_points: draw_mini_trajectory(ax_legend, traj_points, color, x_offset=0.02, y_center=y_center, scale_x=0.35, scale_y=row_height * 0.7) ax_legend.text(0.42, y_center, info['label'], fontsize=7.5, va='center', fontfamily='monospace', color='#333333') y_start = 0.95 - n_legend * row_height + row_height * 0.5 ax_legend.plot(0.18, y_start, 'o', color='#ff0000', markersize=5, markeredgecolor='black', markeredgewidth=0.8, transform=ax_legend.transAxes) ax_legend.text(0.42, y_start, f'Start ({abs(START_LAT):.2f}°S, {START_LON:.2f}°E)', fontsize=7.5, va='center', fontfamily='monospace', color='#333333') ax_info.axhline(y=0.98, xmin=0.0, xmax=1.0, color='#cccccc', linewidth=1.0) ax_info.text(0.02, 0.72, f'Meteorological Data: GDAS 1° | Starting Location: {abs(START_LAT):.4f}°S, {START_LON:.4f}°E', fontsize=6.5, fontfamily='monospace', color='#555555') ax_info.text(0.02, 0.38, f'Backward Duration: 24 hours | Vertical Motion: Omega | Model Top: 20500 m AGL', fontsize=6.5, fontfamily='monospace', color='#555555') ax_info.text(0.5, 0.05, 'NOAA ARL READY — HYSPLIT v5.4.2', fontsize=6, ha='center', fontfamily='monospace', color='#999999') fig.savefig(outpath, dpi=100, bbox_inches='tight', facecolor='white', pad_inches=0.05) plt.close(fig) print(f" OK: {outpath.name}") def plot_grid_ready(): fig, axes = plt.subplots(4, 5, figsize=(18, 15), subplot_kw={'projection': ccrs.PlateCarree()}, facecolor='white') dates = ["20250321", "20250322", "20250323", "20250324", "20250325"] times = ["0000", "0600", "1200", "1800"] time_labels = ["00 UTC", "06 UTC", "12 UTC", "18 UTC"] for col, date in enumerate(dates): for row, time in enumerate(times): ax = axes[row, col] tdump_path = INPUT_DIR / f"tdump_{date}_{time}" if not tdump_path.exists(): ax.set_visible(False) continue trajs, n_levels = parse_tdump(tdump_path) if not trajs: ax.set_visible(False) continue extent = auto_extent(trajs, padding=1.0) ax.set_extent(extent, crs=ccrs.PlateCarree()) ax.set_facecolor(OCEAN_COLORS[0]) ax.add_feature(cfeature.LAND, facecolor=LAND_COLOR, zorder=1) ax.add_feature(cfeature.COASTLINE, linewidth=0.4, color=COAST_COLOR, zorder=3) ax.add_feature(cfeature.BORDERS, linestyle='--', linewidth=0.2, color=BORDER_COLOR, zorder=3) gl = ax.gridlines(linewidth=0.15, color=GRID_COLOR, alpha=0.3, draw_labels=False, zorder=2) for traj_num, points in trajs.items(): if traj_num > n_levels: continue info = LEVEL_INFO.get(traj_num) if not info: continue color = info['color'] lats = [p[0] for p in points] lons = [p[1] for p in points] ax.plot(lons, lats, color=color, linewidth=1.0, alpha=0.9, transform=ccrs.PlateCarree(), zorder=5) mid = len(lons) // 2 if mid < len(lons) - 1: ax.annotate('', xy=(lons[mid+1], lats[mid+1]), xytext=(lons[mid], lats[mid]), arrowprops=dict(arrowstyle='->', color=color, lw=0.8), transform=ccrs.PlateCarree(), zorder=6) ax.plot(START_LON, START_LAT, 'o', color='#ff0000', markersize=4, markeredgecolor='black', markeredgewidth=0.5, transform=ccrs.PlateCarree(), zorder=10) day = date[6:8] mon = date[4:6] ax.set_title(f"{day}/{mon} {time_labels[row]}", fontsize=8, fontweight='bold', pad=3) legend_elements = [ Line2D([0], [0], color=LEVEL_INFO[i]["color"], linewidth=2.5, label=LEVEL_INFO[i]["label"]) for i in range(1, 5) ] legend_elements.append( Line2D([0], [0], marker='o', color='#ff0000', markeredgecolor='black', markeredgewidth=0.8, linestyle='None', markersize=6, label='Start') ) fig.legend(handles=legend_elements, loc='lower center', ncol=5, fontsize=9, framealpha=0.95, edgecolor='#cccccc', bbox_to_anchor=(0.5, -0.01)) fig.suptitle('NOAA HYSPLIT MODEL — Backward Trajectories (24h)\n' f'Kudus (6.79°S, 110.87°E) | GDAS 1° | 21–25 Mar 2025', fontsize=12, fontweight='bold', y=0.98, color='#333333') plt.tight_layout(rect=[0.02, 0.04, 1, 0.95]) outpath = OUTPUT_DIR / "hysplit_grid_ready.png" fig.savefig(outpath, dpi=150, bbox_inches='tight', facecolor='white') plt.close(fig) print(f"Grid: {outpath}") if __name__ == "__main__": print("=== READY Web GIF Clone — Final ===") tdump_files = sorted(INPUT_DIR.glob("tdump_*")) for tdump in tdump_files: outname = tdump.stem + "_ready.png" plot_ready_web(tdump, OUTPUT_DIR / outname) print("\n=== Grid ===") plot_grid_ready() print("\nDone!") |