เมื่อวิศวกรสำรวจคำนวณเรื่องน้ำ – วิเคราะห์ระบบระบายน้ำบริเวณ RESA


เริ่มต้นจากคำถามง่ายๆ

เริ่มจากคำถามว่า “ถ้าฝนตกหนักแบบฝน 50 ปี น้ำพื้นที่ RESA จะระบายทันไหม?”

คำถามนี้ฟังดูเหมือนเป็นงานของวิศวกรโยธาหรือวิศวกรชลประทาน แต่ถ้าคิดดูดีๆ — คนที่มีพื้นผิวออกแบบ (Design Surface) อยู่ในมือ คนที่รู้ว่าน้ำไหลไปทางไหน และคนทำอาชีพที่วัดพื้นที่และระดับมาตลอด — นั่นคือวิศวกรสำรวจ

ผมเรียน Hydrology มาในมหาวิทยาลัย เนื่องจากรู้สึกว่ายากและคิดว่าคงไม่มีโอกาสได้ใช้ เกรดที่ออกมาจึงน่าสงสาร จำได้ว่าได้เกรด C ไปแบบอย่างถูลู่ถูกัง เมื่อสิบกว่าปีที่แล้วได้ใช้ตอนคำนวณขนาดท่อที่จะฝังไว้ใต้ถนนตัดใหม่ โดยคำนวณจากปริมาณน้ำฝนและทราบพื้นที่ที่รองรับน้ำฝนบริเวณที่จะวางท่อนั้นๆ วันนี้จะหยิบมันขึ้นมาทวนความจำอีกครั้ง


North RESA คืออะไร

RESA ย่อมาจาก Runway End Safety Area คือพื้นที่นิรภัยต่อเนื่องจากปลายทางวิ่ง ออกแบบมาเพื่อรองรับกรณีที่อากาศยานวิ่งออกนอกทางวิ่ง (Runway Excursion)

North RESA อยู่ด้านเหนือของสนามบินอู่ตะเภามีขนาด 0.5 ตารางกิโลเมตร พื้นผิวเป็น Grass Turf ลาดเอียง ระบายน้ำออกสองข้างซ้าย-ขวาแบ่งครึ่งผ่าน Trapezoidal Ditch ทั้งสองเส้น ก่อนไหลต่อไปยัง Retention Pond


ตรวจสอบพื้นผิว DEM

ผมมี Top surface จาก Civil 3D ที่ผู้ออกแบบได้ออกแบบไว้บริเวณ RESA สามารส่งออกเป็นรูปแบบ LandXML เพื่อไปโปรแกรมอื่นได้ สามารถใช้ Global Mapper อ่านได้สบายๆ แต่รูปแบบไฟล์ LandXML ผมพยายามนำเข้า QGIS ผ่านทางปลั๊กอิน ไม่สำเร็จมี errors ผมลองปรึกษาคุณคล็อดเอไอ เขาบอกว่าเขาสามารถเขียนสคริปต์ให้โดยแปลงจาก LandXML ไปยัง Geotiff แถมสามารถตัดหรือครอปตามขอบเขตไฟล์ DXF ได้ ผมอุทานเลยเลยมันจะเก่งอะไรขนาดนั้น ใจยังไม่เชื่อ แต่พอเขาทำสคริปต์มาให้ ลองรันรอบแรกมันได้ผลเลย ตอนนี้คิดไม่ออกเลยว่าเอไออีกห้าปี มันจะเก่งขนาดไหน หรือว่ายุคซูปเปอร์แอพใกล้จะถึงแล้ว

#!/usr/bin/env python3
"""
landxml_to_geotiff.py
Convert LandXML TIN surface to GeoTIFF raster (elevation)

Usage:
  python landxml_to_geotiff.py input.xml output.tif [resolution] [epsg] [crop_area.dxf]

Arguments:
  input.xml    : LandXML file containing TIN surface
  output.tif   : Output GeoTIFF file
  resolution   : Grid resolution in metres (default: 0.10)
  epsg         : CRS EPSG code (default: 32647 = UTM Zone 47N Thailand)

Example:
  python landxml_to_geotiff.py NorthRESA.xml NorthRESA_DEM.tif 0.10 32647

Requirements:
  pip install lxml numpy scipy rasterio

Author: based on workflow by Prajuab Riabroy (priabroy.name)
"""

import sys
import numpy as np
from lxml import etree
from scipy.interpolate import LinearNDInterpolator
import rasterio
from rasterio.transform import from_bounds
from rasterio.crs import CRS

# ─── Parse arguments ────────────────────────────────────────────
def usage():
    print(__doc__)
    sys.exit(1)

if len(sys.argv) < 3:
    usage()

input_xml  = sys.argv[1]
output_tif = sys.argv[2]
resolution = float(sys.argv[3]) if len(sys.argv) > 3 else 0.10
epsg       = int(sys.argv[4])   if len(sys.argv) > 4 else 32647

print(f"Input  : {input_xml}")
print(f"Output : {output_tif}")
print(f"Resolution: {resolution} m")
print(f"CRS    : EPSG:{epsg}")

# ─── Parse LandXML ──────────────────────────────────────────────
print("\nParsing LandXML...")
tree = etree.parse(input_xml)
root = tree.getroot()

# Handle namespace — LandXML may or may not have one
ns = root.nsmap.get(None, '')
if ns:
    ns_prefix = f'{{{ns}}}'
else:
    ns_prefix = ''

def tag(name):
    return f'{ns_prefix}{name}'

# Find first Surface
surface = root.find(f'.//{tag("Surface")}')
if surface is None:
    print("ERROR: No <Surface> element found in LandXML file.")
    sys.exit(1)

surface_name = surface.get('name', 'Unknown')
print(f"Surface: {surface_name}")

# ─── Extract Points ──────────────────────────────────────────────
print("Reading points...")
pnts_elem = surface.find(f'.//{tag("Pnts")}')
if pnts_elem is None:
    print("ERROR: No <Pnts> element found.")
    sys.exit(1)

points = {}
for p in pnts_elem.findall(tag('P')):
    pid = int(p.get('id'))
    coords = p.text.strip().split()
    # LandXML point order: N E Z (Northing, Easting, Elevation)
    n, e, z = float(coords[0]), float(coords[1]), float(coords[2])
    points[pid] = (e, n, z)  # store as (X=Easting, Y=Northing, Z)

print(f"  Points: {len(points):,}")

# ─── Extract Faces (Triangles) ──────────────────────────────────
print("Reading faces...")
faces_elem = surface.find(f'.//{tag("Faces")}')
if faces_elem is None:
    print("ERROR: No <Faces> element found.")
    sys.exit(1)

faces = []
for f in faces_elem.findall(tag('F')):
    ids = f.text.strip().split()
    faces.append((int(ids[0]), int(ids[1]), int(ids[2])))

print(f"  Faces : {len(faces):,}")

# ─── Build point arrays ──────────────────────────────────────────
print("\nBuilding interpolator...")
xs = np.array([points[pid][0] for pid in points])
ys = np.array([points[pid][1] for pid in points])
zs = np.array([points[pid][2] for pid in points])

x_min, x_max = xs.min(), xs.max()
y_min, y_max = ys.min(), ys.max()

print(f"  X range: {x_min:.2f} → {x_max:.2f}  ({x_max-x_min:.1f} m)")
print(f"  Y range: {y_min:.2f} → {y_max:.2f}  ({y_max-y_min:.1f} m)")
print(f"  Z range: {zs.min():.3f} → {zs.max():.3f} m")

# ─── Create output grid ──────────────────────────────────────────
cols = int(np.ceil((x_max - x_min) / resolution)) + 1
rows = int(np.ceil((y_max - y_min) / resolution)) + 1
print(f"\nOutput grid: {cols} × {rows} cells at {resolution}m resolution")
print(f"  Estimated size: {cols * rows / 1e6:.1f} million cells")

# Grid coordinates (centre of each cell)
xi = np.linspace(x_min, x_max, cols)
yi = np.linspace(y_max, y_min, rows)  # top-to-bottom (raster convention)
xi_grid, yi_grid = np.meshgrid(xi, yi)

# ─── Interpolate ─────────────────────────────────────────────────
print("\nInterpolating (LinearNDInterpolator)...")
print("  This may take a moment for large surfaces...")

interp = LinearNDInterpolator(list(zip(xs, ys)), zs)
zi = interp(xi_grid, yi_grid)

# Replace NaN (outside TIN boundary) with nodata
nodata = -9999.0
zi[np.isnan(zi)] = nodata

valid_count = np.sum(zi != nodata)
print(f"  Valid cells: {valid_count:,} / {cols*rows:,} ({100*valid_count/(cols*rows):.1f}%)")

# ─── Write GeoTIFF ───────────────────────────────────────────────
print(f"\nWriting GeoTIFF: {output_tif}")
transform = from_bounds(x_min, y_min, x_max, y_max, cols, rows)
crs = CRS.from_epsg(epsg)

with rasterio.open(
    output_tif, 'w',
    driver   = 'GTiff',
    height   = rows,
    width    = cols,
    count    = 1,
    dtype    = np.float32,
    crs      = crs,
    transform= transform,
    nodata   = nodata,
    compress = 'lzw',
) as dst:
    dst.write(zi.astype(np.float32), 1)

print(f"\n✓ Done! Output: {output_tif}")
print(f"  Grid size   : {cols} × {rows}")
print(f"  Resolution  : {resolution} m")
print(f"  CRS         : EPSG:{epsg}")
print(f"  Z min/max   : {zs.min():.3f} / {zs.max():.3f} m")
print(f"\nReady to open in QGIS or use with dem_viewer.html")

เมื่อได้ DEM แบบ Geotiff มาแล้วผมขอคุณคล็อดเอไอ ช่วยร่าง 3D Viewer มาให้เพื่อแสดงผลในเวบ เขาร่างรอบเดียวใช้งานได้ผ่านสบาย(อีกแล้ว) ผู้อ่านสามารถดู DEM ได้ผ่านทาง 3D Viewer ด้านล่าง ซูม หมุน เลื่อนได้



ขั้นตอนที่ 1 — น้ำฝนรอบ 50 ปี (IDF Curve สถานีระยอง)

พื้นที่ขนาด 0.5 ตารางกิโลเมตรมี ผมเลือกใช้ Duration 1 ชั่วโมง ซึ่งให้ Intensity = 130 มม./ชม. จาก IDF Curve สถานีระยอง รอบปีที่ 50

พารามิเตอร์ค่าหมายเหตุ
C (Grass Turf ลาดเอียง)0.50ไม่ใช่ 0.92 แบบลานคอนกรีต
i (50yr, 1hr)130 มม./ชม.IDF Curve ระยอง
A (พื้นที่ North RESA)47.7 เฮกตาร์0.477 กม²
tc1 ชั่วโมง

คำนวณ Peak Flow Rate:

$$Q = \frac{C \times i \times A}{360} = \frac{0.50 \times 130 \times 47.7}{360} = 8.16 \text{ ม}^3/\text{วินาที}$$

ปริมาตรน้ำท่าทั้งหมดตลอด 6 ชั่วโมง:

$$V_{runoff} = Q \times t_c \times 3600 = 8.16 \times 6 \times 3600 = 30,966 \text{ ม}^3$$

แบ่งซ้าย-ขวา 50/50:

รวมต่อด้าน
Q8.61 ม³/วินาที4.30 ม³/วินาที
V_runoff30,996 ม³15,498 ม³

ขั้นตอนที่ 2 — ตรวจสอบ Trapezoidal Ditch ด้วย Manning’s Equation

ระบบระบายน้ำ North RESA มี Trapezoidal Ditch สองเส้น (ซ้าย-ขวา) รับน้ำจาก RESA แล้วระบายต่อไปยัง Retention Pond

ข้อมูลหน้าตัด Trapezoidal Ditch:

ด้านล่างกว้าง (b)    = 2.0 ม.
ความลึก (d)        = 2.0 ม.
ด้านบนกว้าง         = 8.0 ม.
Side slope (z)    = 1.5:1
พื้นผิว             = Concrete lined (n = 0.013)
Slope (S)        = 1.5% = 0.015

คำนวณตามลำดับ:

พื้นที่หน้าตัด: $$A = (b + z \times d) \times d = (2 + 1.5 \times 2) \times 2 = 10 \text{ ม}^2$$

Wetted Perimeter: $$P = b + 2d\sqrt{1+z^2} = 2 + 2(2)\sqrt{1+1.5^2} = 9.211 \text{ ม.}$$

Hydraulic Radius: $$R = \frac{A}{P} = \frac{10}{9.211} = 1.086 \text{ ม.}$$

Manning’s Equation: $$Q_{ditch} = \frac{1}{n} \times A \times R^{2/3} \times S^{1/2} = \frac{1}{0.013} \times 10 \times 1.056 \times 0.1225 = 99.5 \text{ ม}^3/\text{วินาที}$$

เปรียบเทียบ:

ค่า
Q ที่ต้องการ (ต่อด้าน)4.30 ม³/วินาที
Q ที่รับได้ (Ditch)99.5 ม³/วินาที
Safety Ratio23.1 เท่า

Ditch รับได้มากกว่าที่ต้องการถึง 23.1 เท่า — ผ่านเกณฑ์ Hydraulic Capacity อย่างสบาย


ข้อควรระวัง — Velocity สูงเกินไป

แม้ว่าขนาด Ditch จะใหญ่พอ แต่ด้วย Slope 1.5% ทำให้ Flow Velocity สูงถึง:

$$V = \frac{Q}{A} = \frac{99.5}{10} = 9.95 \text{ ม./วินาที}$$

ค่านี้เกินมาตรฐานสูงสุดสำหรับ Concrete lined ที่แนะนำไว้ที่ 6–8 ม./วินาที ซึ่งอาจทำให้เกิดการกัดเซาะที่จุดต่อและปลายทาง ควรพิจารณาติดตั้ง Energy Dissipator หรือ Stilling Basin ที่ปลาย Ditch


สรุป

หัวข้อผล
แอ่งน้ำบนพื้นผิว RESAต้องตรวจสอบจาก QGIS Analysis
Q ฝนรอบ 50 ปี (ต่อด้าน)4.30 ม³/วินาที
Trapezoidal Ditch Capacity99.5 ม³/วินาที ✓ ผ่าน
Velocity9.95 ม./วินาที ⚠ สูง — ต้องตรวจสอบ

Trapezoidal Ditch มีขนาดเพียงพอรับน้ำจาก RESA ในสภาวะฝน 50 ปีได้อย่างสบาย แต่ความเร็วน้ำที่สูงเป็นสิ่งที่ผู้ออกแบบได้แก้ไขมาแล้วโดยออกแบบ Energy Dissipator สำหรับรางน้ำที่มีความชันมากกว่า 1.5%


บทส่งท้าย

วิศวกรสำรวจมีพื้นผิว DEM อยู่ในมือ มีข้อมูลเพียงพอที่จะตอบคำถามสำคัญได้ว่า ฝน 50 ปี รางระบายน้ำรับได้ — แต่ต้องพิจารณาความเร็วของน้ำไหลด้วย

เราตอบคำถามได้แล้วว่า Trapezoidal Ditch มีขนาดเพียงพอรับน้ำฝนรอบ 50 ปีจาก North RESA หรือไม่ แต่นั่นเป็นเพียงการตรวจสอบ ระบบระบายน้ำ เท่านั้น ยังมีคำถามที่สองที่ยังไม่ได้ตอบ — บนพื้นผิว RESA เองนั้น น้ำไหลออกไปยัง Ditch ได้จริงหรือไม่ หรือมีจุดใดที่น้ำขังอยู่โดยไม่มีทางออก?

ในการก่อสร้างสนามบิน แม้แต่แอ่งน้ำเล็กน้อยบนพื้นผิวก็อาจเป็นปัญหาต่อการปฏิบัติงานและความปลอดภัย ใน QGIS มีอัลกอริทึม Fill Sinks (Wang & Liu) เพื่อสแกนพื้นผิวออกแบบทุก Cell แล้วคำนวณความลึกและปริมาตรของแอ่งน้ำทุกจุด ก่อนนำมาเปรียบเทียบกับเกณฑ์มาตรฐานเพื่อตอบคำถามนี้อย่างสมบูรณ์ได้ สำหรับ North RESA ไม่มีปัญหาเรื่องนี้ แต่ทางขับและทางวิ่งที่ออกแบบมาแล้ว จะนำ Surface มาคำนวณด้วยอัลกอริทึม Fill Sinks เพื่อให้มั่นใจว่าฝนตกแล้วน้ำจะไม่ขัง

ฉบับนี้คงพอแค่นี้ก่อน หอมปากหอมคอ ผมเพิ่งจะเข้าใจว่าทำไมหลักสูตรวศบ.สำรวจ ที่ผมเคยเรียนทำไมต้องมี geology อยู่ เพราะมันความเกี่ยวพันเกี่ยวเนื่องกันอย่างนี้นี่เองครับ


Leave a Reply

Your email address will not be published. Required fields are marked *