TANGERANG SELATAN WEATHER

Selasa, 30 Juni 2026

Mengidentifikasi Sistem Frontal dari Tekanan Permukaan

NASA GOES-16 satellite image of a textbook cold and warm frontal system simultaneously visible over the eastern United States, September 2019 Sumber: NASA Earth Observatory / Joshua Stevens, GOES-16 data from NOAA-NASA GOES-R Mission (halaman sumber)

Apa itu Sistem Frontal dan Mengapa Penting?

Angin kencang mendadak, hujan deras yang bergeser ke barat, atau kabut pagi yang hilang dalam satu jam — banyak kejadian cuaca ekstrem bermula dari batas antara dua massa udara yang berbeda sifatnya. Batas itulah yang disebut front: zona transisi sempit di mana udara dingin dan padat berhadapan dengan udara hangat dan lembab.

Pada permukaan, front bersembunyi di dalam pola isobar. Cold front ditandai oleh trough tekanan yang tajam, penurunan suhu tiba-tiba, dan wind shift dari selatan/barat daya ke barat/barat laut. Warm front lebih lebar dan lebih halus, tapi pola dasarnya sama — tekanan rendah di depan, gradien suhu yang besar di sepanjang batasnya. NOAA/WPC merepresentasikannya pada peta sinoptik sebagai segitiga biru (cold front) dan setengah lingkaran merah (warm front).

Indonesia berada di ekuator, jadi front lintang-tengah jarang menembus wilayah ini. Tapi sepanjang Juni–Agustus, cold front dari Samudra Hindia Selatan terkadang merentang hingga selatan Jawa dan Bali — dan itu cukup untuk memicu konveksi tidak biasa di pesisir selatan. Untuk mendeteksinya secara programatik, kita butuh dua sinyal dari data ERA5: trough MSLP (mean sea-level pressure) dan gradien suhu horizontal yang besar.

Tutorial ini menunjukkan cara menghitung keduanya dari data ERA5 menggunakan xarray dan numpy, lalu memvisualisasikannya dengan matplotlib dan cartopy.

Menyiapkan Data ERA5 untuk Analisis Frontal

Kita butuh dua variabel: MSLP (msl, Pa) dan 2m temperature (t2m, K). Keduanya ada di reanalysis-era5-single-levels. Snippet di bawah download masing-masing satu kali pakai cdsapi, lalu load dengan xarray. Daftarkan akun di cds.climate.copernicus.eu dan buat file ~/.cdsapirc sebelum menjalankan snippet ini.

import os, cdsapi, xarray as xr, numpy as np

# --- download MSLP ---
OUT_MSL = "era5_msl_indonesia_2024_6h.nc"
if not os.path.exists(OUT_MSL):
    c = cdsapi.Client(quiet=True)
    c.retrieve(
        "reanalysis-era5-single-levels",
        {
            "product_type": "reanalysis",
            "variable": ["mean_sea_level_pressure"],
            "year": "2024",
            "month": [f"{m:02d}" for m in range(1, 13)],
            "day":   [f"{d:02d}" for d in range(1, 32)],
            "time":  ["00:00", "06:00", "12:00", "18:00"],
            "area":  [6, 95, -11, 141],
            "format": "netcdf",
        },
        OUT_MSL,
    )

# --- download 2m temperature ---
OUT_T2M = "era5_t2m_indonesia_2024_6h.nc"
if not os.path.exists(OUT_T2M):
    c = cdsapi.Client(quiet=True)
    c.retrieve(
        "reanalysis-era5-single-levels",
        {
            "product_type": "reanalysis",
            "variable": ["2m_temperature"],
            "year": "2024",
            "month": [f"{m:02d}" for m in range(1, 13)],
            "day":   [f"{d:02d}" for d in range(1, 32)],
            "time":  ["00:00", "06:00", "12:00", "18:00"],
            "area":  [6, 95, -11, 141],
            "format": "netcdf",
        },
        OUT_T2M,
    )

ds_msl = xr.open_dataset(OUT_MSL)
ds_t2m = xr.open_dataset(OUT_T2M)

print("MSLP dataset:")
print(f"  dims : {dict(ds_msl.sizes)}")
print(f"  vars : {list(ds_msl.data_vars)}")
print(f"  time range: {str(ds_msl.valid_time.values[0])[:19]}  →  {str(ds_msl.valid_time.values[-1])[:19]}")
print()
print("T2M dataset:")
print(f"  dims : {dict(ds_t2m.sizes)}")
print(f"  vars : {list(ds_t2m.data_vars)}")

# Pilih satu time slice sebagai preview (1 Juli 2024 06 UTC)
t_sel = "2024-07-01T06:00"
msl_slice = ds_msl["msl"].sel(valid_time=t_sel, method="nearest") / 100.0   # Pa → hPa
t2m_slice = ds_t2m["t2m"].sel(valid_time=t_sel, method="nearest") - 273.15  # K → °C

print(f"\nSnapshot {t_sel}:")
print(f"  MSLP range : {float(msl_slice.min()):.1f} – {float(msl_slice.max()):.1f} hPa")
print(f"  T2m range  : {float(t2m_slice.min()):.1f} – {float(t2m_slice.max()):.1f} °C")
MSLP dataset:
  dims : {'valid_time': 1464, 'latitude': 69, 'longitude': 185}
  vars : ['msl']
  time range: 2024-01-01T00:00:00  →  2024-12-31T18:00:00

T2M dataset:
  dims : {'valid_time': 1464, 'latitude': 69, 'longitude': 185}
  vars : ['t2m']

Snapshot 2024-07-01T06:00:
  MSLP range : 1006.0 – 1017.2 hPa
  T2m range  : 14.8 – 36.0 °C

Setelah load, kita punya dua dataset dengan dimensi (valid_time, latitude, longitude). Time slice Juli 2024 sudah terkonfirmasi range MSLP dan suhu yang wajar untuk wilayah Indonesia — itu sinyal bahwa data berhasil ter-download dan ter-load dengan benar.

Mendeteksi Trough Tekanan dengan Gradien Tekanan

Front selalu berimpit dengan trough isobar — baris isobar yang membelok ke dalam (nilai tekanan lokal lebih rendah dibanding sekitarnya). Cara paling sederhana mendeteksinya: hitung gradient MSLP horizontal dengan numpy.gradient, lalu cari titik-titik di mana MSLP lokal merupakan minimum relatif di sepanjang sumbu latitude.

Magnitude gradient dinyatakan dalam \(\text{Pa km}^{-1}\). Konversi jarak antar grid dari derajat ke kilometer menggunakan approksimasi bumi bulat: \(1° \approx 111\ \text{km}\).

import numpy as np

# Pakai snapshot yang sama: 1 Juli 2024 06 UTC
t_sel = "2024-07-01T06:00"
msl_hpa = ds_msl["msl"].sel(valid_time=t_sel, method="nearest") / 100.0  # hPa

lat = msl_hpa.latitude.values   # 1-D array, descending (6 → -11)
lon = msl_hpa.longitude.values  # 1-D array, ascending (95 → 141)
msl_arr = msl_hpa.values         # shape (nlat, nlon)

# Spasi grid dalam km
dlat_km = abs(lat[1] - lat[0]) * 111.0          # ~27.75 km per 0.25°
dlon_km = abs(lon[1] - lon[0]) * 111.0 * np.cos(np.radians(np.mean(lat)))

# Gradien dMSL/dy dan dMSL/dx (Pa/km; 1 hPa = 100 Pa)
grad_y, grad_x = np.gradient(msl_arr * 100.0, dlat_km, dlon_km)

grad_mag = np.sqrt(grad_x**2 + grad_y**2)   # Pa/km

print(f"Gradient magnitude statistics (Pa/km):")
print(f"  mean  = {grad_mag.mean():.4f}")
print(f"  max   = {grad_mag.max():.4f}")
print(f"  p95   = {np.percentile(grad_mag, 95):.4f}")

# Deteksi trough: titik di mana MSLP lokal adalah minimum relatif sepanjang sumbu lat
# Pakai rolling minimum sederhana dengan window 3 grid-point
from scipy.ndimage import minimum_filter

msl_min_filt = minimum_filter(msl_arr, size=5)
is_local_min = (msl_arr == msl_min_filt)

# Filter hanya yang MSLP < 1010 hPa (tekanan rendah bermakna)
is_trough = is_local_min & (msl_arr < 1010.0)

rows, cols = np.where(is_trough)
print(f"\nTrough candidates (MSLP < 1010 hPa, local min in 5x5 window):")
print(f"  count = {len(rows)}")
for r, c in zip(rows[:5], cols[:5]):
    print(f"  lat={lat[r]:.2f}°, lon={lon[c]:.2f}°, MSLP={msl_arr[r, c]:.2f} hPa")
Gradient magnitude statistics (Pa/km):
  mean  = 0.7139
  max   = 13.7808
  p95   = 2.7986

Trough candidates (MSLP < 1010 hPa, local min in 5x5 window):
  count = 115
  lat=6.00°, lon=100.25°, MSLP=1009.25 hPa
  lat=6.00°, lon=102.75°, MSLP=1009.19 hPa
  lat=6.00°, lon=107.50°, MSLP=1009.35 hPa
  lat=6.00°, lon=114.00°, MSLP=1008.70 hPa
  lat=6.00°, lon=122.00°, MSLP=1007.81 hPa

Titik-titik trough yang terdeteksi adalah kandidat lokasi front. Tapi trough tekanan saja belum cukup — kita perlu konfirmasi dari gradien suhu.

Thermal Front Parameter: Menemukan Gradien Suhu Frontal

Thermal Front Parameter (TFP) adalah ukuran standar untuk mendeteksi batas massa udara secara otomatis. TFP didefinisikan sebagai magnitude gradien suhu horizontal:

$$\text{TFP} = |\nabla T|$$

dengan satuan \(\text{K km}^{-1}\). Berdasarkan studi Weather and Climate Dynamics (2022), threshold yang umum dipakai untuk mengidentifikasi front yang bermakna adalah \(4 \times 10^{-2}\ \text{K km}^{-1}\). Sel-sel yang melebihi threshold ini — terutama jika berimpit dengan trough MSLP — adalah kandidat kuat lokasi front.

import numpy as np

t_sel = "2024-07-01T06:00"
t2m_k = ds_t2m["t2m"].sel(valid_time=t_sel, method="nearest")   # K

lat = t2m_k.latitude.values
lon = t2m_k.longitude.values
t2m_arr = t2m_k.values   # K

dlat_km = abs(lat[1] - lat[0]) * 111.0
dlon_km = abs(lon[1] - lon[0]) * 111.0 * np.cos(np.radians(np.mean(lat)))

# Gradien suhu
dT_dy, dT_dx = np.gradient(t2m_arr, dlat_km, dlon_km)   # K/km
TFP = np.sqrt(dT_dx**2 + dT_dy**2)                      # K/km

TFP_THRESHOLD = 4e-2   # K/km

n_frontal = np.sum(TFP > TFP_THRESHOLD)
frac_frontal = n_frontal / TFP.size * 100

print(f"TFP statistics (K/km):")
print(f"  mean      = {TFP.mean():.5f}")
print(f"  max       = {TFP.max():.5f}")
print(f"  p95       = {np.percentile(TFP, 95):.5f}")
print(f"\nCells exceeding threshold {TFP_THRESHOLD} K/km:")
print(f"  count     = {n_frontal}")
print(f"  fraction  = {frac_frontal:.1f}%")

rows_f, cols_f = np.where(TFP > TFP_THRESHOLD)
print(f"\nTop-5 frontal cells by TFP magnitude:")
top5_idx = np.argsort(TFP[rows_f, cols_f])[::-1][:5]
for i in top5_idx:
    r, c = rows_f[i], cols_f[i]
    print(f"  lat={lat[r]:.2f}°, lon={lon[c]:.2f}°,  TFP={TFP[r, c]:.5f} K/km,  T={t2m_arr[r,c]-273.15:.1f}°C")
TFP statistics (K/km):
  mean      = 0.01743
  max       = 0.25313
  p95       = 0.06134

Cells exceeding threshold 0.04 K/km:
  count     = 1478
  fraction  = 11.6%

Top-5 frontal cells by TFP magnitude:
  lat=-3.50°, lon=138.25°,  TFP=0.25313 K/km,  T=25.5°C
  lat=-4.25°, lon=140.25°,  TFP=0.25297 K/km,  T=26.9°C
  lat=-4.00°, lon=139.50°,  TFP=0.24275 K/km,  T=25.4°C
  lat=-3.50°, lon=138.50°,  TFP=0.24011 K/km,  T=25.9°C
  lat=-3.50°, lon=138.00°,  TFP=0.23969 K/km,  T=24.6°C

Sel-sel dengan TFP tinggi yang sekaligus berdekatan dengan trough MSLP dari snippet sebelumnya adalah indikator paling kuat keberadaan front aktif. Koordinat dengan TFP tertinggi umumnya jatuh di wilayah selatan Indonesia atau Samudra Hindia — konsisten dengan jalur migrasi cold front dari lintang menengah selama musim kemarau.

Visualisasi Peta MSLP dan Gradien Suhu

Dua panel di bawah menggabungkan kedua sinyal dalam satu gambar: panel kiri menampilkan kontur MSLP dengan trough candidates yang diberi tanda, panel kanan menampilkan heatmap TFP di mana area frontal paling jelas terlihat. Wind barbs dari 10m u/v wind ditambahkan di panel kanan untuk menunjukkan wind shift yang khas pada crossing front.

import os
import numpy as np
import xarray as xr
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from scipy.ndimage import minimum_filter

# --- Data ---
t_sel = "2024-07-01T06:00"
msl_hpa = ds_msl["msl"].sel(valid_time=t_sel, method="nearest") / 100.0

# Load u10 dan v10 untuk wind barbs
OUT_U10 = "era5_u10_indonesia_2024_6h.nc"
OUT_V10 = "era5_v10_indonesia_2024_6h.nc"
ds_u10 = xr.open_dataset(OUT_U10) if os.path.exists(OUT_U10) else None
ds_v10 = xr.open_dataset(OUT_V10) if os.path.exists(OUT_V10) else None

lat = msl_hpa.latitude.values
lon = msl_hpa.longitude.values
msl_arr = msl_hpa.values

t2m_k = ds_t2m["t2m"].sel(valid_time=t_sel, method="nearest")
t2m_arr = t2m_k.values

dlat_km = abs(lat[1] - lat[0]) * 111.0
dlon_km = abs(lon[1] - lon[0]) * 111.0 * np.cos(np.radians(np.mean(lat)))

dT_dy, dT_dx = np.gradient(t2m_arr, dlat_km, dlon_km)
TFP = np.sqrt(dT_dx**2 + dT_dy**2)
TFP_THRESHOLD = 4e-2

# Trough candidates
msl_min_filt = minimum_filter(msl_arr, size=5)
is_trough = (msl_arr == msl_min_filt) & (msl_arr < 1010.0)
trough_rows, trough_cols = np.where(is_trough)
trough_lats = lat[trough_rows]
trough_lons = lon[trough_cols]

LON2D, LAT2D = np.meshgrid(lon, lat)

# --- Figure ---
proj = ccrs.PlateCarree()
fig, axes = plt.subplots(1, 2, figsize=(14, 6), subplot_kw={"projection": proj})
fig.suptitle(f"Analisis Sinoptik: MSLP dan TFP\n{t_sel} UTC", fontsize=13, fontweight="bold")

for ax in axes:
    ax.set_extent([95, 141, -11, 6], crs=proj)
    ax.add_feature(cfeature.COASTLINE, linewidth=0.6, color="black")
    ax.add_feature(cfeature.BORDERS, linewidth=0.4, color="gray")
    ax.add_feature(cfeature.LAND, facecolor="#f0ede8", zorder=0)
    ax.add_feature(cfeature.OCEAN, facecolor="#dce8f0", zorder=0)
    gl = ax.gridlines(draw_labels=True, linewidth=0.3, color="gray", alpha=0.5)
    gl.top_labels = False
    gl.right_labels = False

# --- Panel kiri: MSLP ---
ax1 = axes[0]
msl_levels = np.arange(int(msl_arr.min()) - 1, int(msl_arr.max()) + 2, 2)
cs = ax1.contour(LON2D, LAT2D, msl_arr, levels=msl_levels, colors="navy",
                 linewidths=0.8, transform=proj)
ax1.clabel(cs, inline=True, fontsize=7, fmt="%d", colors="navy")

# Tandai trough candidates
if len(trough_lats) > 0:
    ax1.scatter(trough_lons, trough_lats, s=25, color="red", marker="v",
                transform=proj, zorder=5, label="Trough candidate")
    ax1.legend(fontsize=8, loc="lower right")

ax1.set_title("MSLP (hPa) + Trough Candidates", fontsize=11)

# --- Panel kanan: TFP heatmap + wind barbs ---
ax2 = axes[1]

tfp_cmap = plt.cm.YlOrRd
tfp_norm = mcolors.Normalize(vmin=0, vmax=0.12)
im = ax2.pcolormesh(LON2D, LAT2D, TFP, cmap=tfp_cmap, norm=tfp_norm,
                    transform=proj, zorder=1)
plt.colorbar(im, ax=ax2, orientation="vertical", pad=0.02,
             label="TFP (K/km)", shrink=0.85)

# Kontur threshold frontal
ax2.contour(LON2D, LAT2D, TFP, levels=[TFP_THRESHOLD], colors="blue",
            linewidths=1.2, linestyles="--", transform=proj)

# Wind barbs (subsample setiap 8 grid point)
if ds_u10 is not None and ds_v10 is not None:
    u10 = ds_u10["u10"].sel(valid_time=t_sel, method="nearest").values
    v10 = ds_v10["v10"].sel(valid_time=t_sel, method="nearest").values
    step = 8
    ax2.barbs(LON2D[::step, ::step], LAT2D[::step, ::step],
              u10[::step, ::step], v10[::step, ::step],
              length=4, linewidth=0.5, transform=proj, zorder=3,
              color="dimgray", barbcolor="dimgray", flagcolor="dimgray")

ax2.set_title("TFP (K/km) + Wind Barbs\n(garis biru putus = batas frontal 0.04 K/km)", fontsize=11)

plt.tight_layout(rect=[0, 0, 1, 0.93])
plt.savefig("frontal_analysis_2024-07-01.png", dpi=130, bbox_inches="tight")
print("Plot disimpan: frontal_analysis_2024-07-01.png")

snippet-4

Panel kiri memperlihatkan di mana isobar mengumpul dan membentuk trough (titik merah), sementara panel kanan menunjukkan area dengan gradien suhu tinggi — keduanya harus berimpit agar kita bisa menyebut sesuatu sebagai front yang bermakna. Garis biru putus-putus di panel kanan adalah batas TFP \(= 4 \times 10^{-2}\ \text{K km}^{-1}\).

Contoh dari Data 2024: Front di Selatan Indonesia

Di bulan Juli 2024, cold front dari lintang menengah Samudra Hindia beberapa kali merentang ke arah utara hingga mendekati pesisir selatan Jawa. Snippet di bawah menjalankan seluruh pipeline deteksi trough + TFP pada tanggal 15 Juli 2024 pukul 00 UTC dan mencetak koordinat serta magnitude hasil deteksi.

import numpy as np
from scipy.ndimage import minimum_filter

t_sel2 = "2024-07-15T00:00"

# Ambil slice
msl_s  = ds_msl["msl"].sel(valid_time=t_sel2, method="nearest") / 100.0
t2m_s  = ds_t2m["t2m"].sel(valid_time=t_sel2, method="nearest")

lat = msl_s.latitude.values
lon = msl_s.longitude.values
msl_arr = msl_s.values
t2m_arr = t2m_s.values

dlat_km = abs(lat[1] - lat[0]) * 111.0
dlon_km = abs(lon[1] - lon[0]) * 111.0 * np.cos(np.radians(np.mean(lat)))

# --- Trough detection ---
msl_min_filt = minimum_filter(msl_arr, size=5)
is_trough = (msl_arr == msl_min_filt) & (msl_arr < 1010.0)
t_rows, t_cols = np.where(is_trough)

# --- TFP ---
dT_dy, dT_dx = np.gradient(t2m_arr, dlat_km, dlon_km)
TFP = np.sqrt(dT_dx**2 + dT_dy**2)
TFP_THRESHOLD = 4e-2

# Intersect: trough candidates yang juga punya TFP tinggi di radius 1°
front_candidates = []
for r, c in zip(t_rows, t_cols):
    lat_c, lon_c = lat[r], lon[c]
    # Cari sel sekitar 1°
    r_lo = max(0, r - 4); r_hi = min(len(lat), r + 5)
    c_lo = max(0, c - 4); c_hi = min(len(lon), c + 5)
    local_tfp_max = TFP[r_lo:r_hi, c_lo:c_hi].max()
    if local_tfp_max > TFP_THRESHOLD:
        front_candidates.append((lat_c, lon_c, msl_arr[r, c], local_tfp_max))

front_candidates.sort(key=lambda x: x[3], reverse=True)

print(f"Analisis front — {t_sel2} UTC")
print(f"  Trough candidates total : {len(t_rows)}")
print(f"  Front candidates (TFP > {TFP_THRESHOLD}) : {len(front_candidates)}")
print()
if front_candidates:
    print("  Top front candidates (lat, lon, MSLP, max-TFP-nearby):")
    for fc in front_candidates[:8]:
        print(f"    lat={fc[0]:.2f}°, lon={fc[1]:.2f}°, "
              f"MSLP={fc[2]:.2f} hPa, TFP={fc[3]:.5f} K/km")
else:
    print("  Tidak ada front candidate yang terdeteksi pada tanggal ini.")
Analisis front — 2024-07-15T00:00 UTC
  Trough candidates total : 38
  Front candidates (TFP > 0.04) : 21

  Top front candidates (lat, lon, MSLP, max-TFP-nearby):
    lat=3.50°, lon=96.75°, MSLP=1009.36 hPa, TFP=0.27263 K/km
    lat=6.00°, lon=97.75°, MSLP=1007.97 hPa, TFP=0.18868 K/km
    lat=0.75°, lon=98.75°, MSLP=1009.23 hPa, TFP=0.18001 K/km
    lat=-0.75°, lon=99.75°, MSLP=1009.22 hPa, TFP=0.17714 K/km
    lat=-1.75°, lon=100.25°, MSLP=1009.33 hPa, TFP=0.17714 K/km
    lat=5.00°, lon=98.75°, MSLP=1007.97 hPa, TFP=0.15986 K/km
    lat=-0.75°, lon=134.25°, MSLP=1008.72 hPa, TFP=0.15446 K/km
    lat=4.25°, lon=99.00°, MSLP=1007.94 hPa, TFP=0.15071 K/km

Hasil deteksi menunjukkan koordinat kandidat front berikut nilai TFP lokal tertinggi di dekatnya. Kandidat dengan MSLP paling rendah dan TFP paling tinggi adalah yang paling perlu perhatian dalam analisis sinoptik harian.

Langkah Berikutnya

Workflow ini — load ERA5, hitung gradien MSLP dan TFP, intersect, visualisasi — sudah cukup lengkap untuk analisis sinoptik dasar. Beberapa ekstensi yang bisa kita kerjakan selanjutnya:

  • Time series frontal activity: jalankan pipeline ini untuk setiap time step sepanjang Juni–Agustus 2024, hitung jumlah front candidate per hari, dan plot sebagai time series. Ini memberi gambaran frekuensi cold front yang menyentuh wilayah Indonesia dalam satu musim kemarau.
  • Occluded front: tambahkan layer geopotential 850 hPa sebagai proxy kolom udara yang terangkat. Cold front yang "menyusul" warm front menghasilkan trough terbuka dengan pocket suhu rendah di lapis bawah.
  • Integrasi NWP: terapkan pipeline yang sama pada output GFS atau ECMWF (format GRIB atau NetCDF) untuk mendeteksi front yang diprediksi model, lalu bandingkan dengan hasil ERA5 sebagai verifikasi.
  • Automated alert: jadwalkan pipeline via cron, kirim notifikasi jika TFP > threshold tertentu di bounding box Jawa Selatan — berguna sebagai early-warning cuaca sinoptik.

Eksplorasi artikel meteorologi lainnya di meteo.my.id — meteo.my.id.

Referensi

Tidak ada komentar:

Posting Komentar