surf / examples /waimea.py
phanerozoic's picture
surf: NSW + WCSPH CUDA kernels, card
1d0ab56 verified
Raw
History Blame
7.08 kB
"""Waimea Bay, driven by the live NDBC 51201 buoy, through the surf kernel.
python waimea.py # live sea state off the bay mouth
python waimea.py --winter # a representative Eddie-day swell
python waimea.py --hs 4 --tp 14 --frames 90
Buoy 51201 (Waimea Bay, HI) sits in ~200 m of water outside the bay and reports
swell and wind-wave trains separately, so the wavemaker below drives both: a
long-period groundswell plus the shorter local windswell riding on it. The North
Shore is seasonal. From roughly April to October 51201 reads 1-2 m and Waimea is
a swimming beach; the bay only does what it is famous for when the winter North
Pacific sends 6 m at 17 s.
The bathymetry here is an idealized Waimea: the real bay's dimensions (a ~600 m
mouth, the deep channel the river cuts on the east side where the rip drains, the
reef shelf on the west that makes the peak, and the notoriously steep shorebreak)
in analytic form. Drop a NOAA Hawaii coastal DEM into `bed()` for the true one.
"""
import argparse
import math
import urllib.request
import numpy as np
import torch
from kernels import get_kernel
BUOY = "https://www.ndbc.noaa.gov/data/realtime2/51201.spec"
G = 9.81
HDRY = 1e-3
# A representative Eddie-day sea state at 51201: the contest needs 20 ft Hawaiian
# faces, which shows up on the buoy as roughly 7.5 m at 17 s out of the NNW.
WINTER = dict(swh=7.5, swp=17.0, wwh=1.2, wwp=8.0, mwd=325.0)
def fetch_sea_state(timeout=20):
"""Latest swell + wind-wave partition from NDBC 51201."""
with urllib.request.urlopen(BUOY, timeout=timeout) as r:
rows = [l.split() for l in r.read().decode().splitlines() if not l.startswith("#")]
for row in rows: # newest first; skip rows with missing data
try:
swh, swp, wwh, wwp = float(row[6]), float(row[7]), float(row[8]), float(row[9])
mwd = float(row[14])
except (ValueError, IndexError):
continue
return dict(swh=swh, swp=swp, wwh=wwh, wwp=wwp, mwd=mwd,
when=f"{row[0]}-{row[1]}-{row[2]} {row[3]}:{row[4]}Z")
raise RuntimeError("no complete observation in 51201.spec")
def bed(X, Y, Lx, Ly):
"""Idealized Waimea Bay bed elevation (m, negative = below sea level)."""
# cross-shore: deep outside, reef shelf, then a steep 1:10 beach face
b = -30.0 + 22.0 * torch.sigmoid((X - 300.0) / 60.0) # 30 m -> ~8 m shelf
b = b + ((X - 460.0) / 10.0).clamp(min=0.0) # steep shorebreak
# the west reef: a shallower lobe that trips the peak first
reef = torch.exp(-(((X - 400.0) / 90.0) ** 2 + ((Y - 0.72 * Ly) / 110.0) ** 2))
b = b + 3.2 * reef
# the east channel: the river mouth stays deep, so the rip drains out here
chan = torch.exp(-((Y - 0.18 * Ly) / 70.0) ** 2) * torch.sigmoid((X - 280.0) / 50.0)
b = b - 5.0 * chan
return b.clamp(max=4.0).contiguous()
def main():
p = argparse.ArgumentParser()
p.add_argument("--winter", action="store_true", help="representative Eddie-day swell")
p.add_argument("--hs", type=float, help="override significant swell height (m)")
p.add_argument("--tp", type=float, help="override swell period (s)")
p.add_argument("--n", type=int, default=1024, help="cross-shore cells")
p.add_argument("--frames", type=int, default=60)
p.add_argument("--out", default="waimea")
a = p.parse_args()
if a.winter:
ss = dict(WINTER, when="preset")
else:
ss = fetch_sea_state()
if a.hs:
ss["swh"] = a.hs
if a.tp:
ss["swp"] = a.tp
print(f"51201 Waimea Bay [{ss['when']}]: swell {ss['swh']:.1f} m @ {ss['swp']:.1f} s, "
f"windwave {ss['wwh']:.1f} m @ {ss['wwp']:.1f} s, MWD {ss['mwd']:.0f} deg")
if not a.winter and ss["swh"] + ss["wwh"] < 2.0:
print(" (the bay is flat today: North Shore is seasonal. try --winter)")
surf = get_kernel("phanerozoic/surf", version=1, trust_remote_code=True)
dev = "cuda"
Nx = a.n
Ny = int(a.n * 0.75)
Lx, Ly = 800.0, 600.0
dx, dy = Lx / Nx, Ly / Ny
xs = torch.linspace(0, Lx, Nx, device=dev)
ys = torch.linspace(0, Ly, Ny, device=dev)
X, Y = torch.meshgrid(xs, ys, indexing="xy")
b = bed(X, Y, Lx, Ly)
h = (0.0 - b).clamp(min=0)
hu = torch.zeros_like(h)
hv = torch.zeros_like(h)
foam = torch.zeros_like(h)
h0 = 30.0
dt = 0.30 * dx / math.sqrt(G * (h0 + ss["swh"]))
cf = 0.004
# obliquity: MWD is the direction waves come FROM, shore normal faces ~340 deg
theta = math.radians(((ss["mwd"] - 340.0 + 180.0) % 360.0) - 180.0)
a_sw, om_sw = ss["swh"] / 2.0, 2 * math.pi / max(ss["swp"], 1.0)
a_ww, om_ww = ss["wwh"] / 2.0, 2 * math.pi / max(ss["wwp"], 1.0)
k_sw = om_sw / math.sqrt(G * h0)
k_ww = om_ww / math.sqrt(G * h0)
xrel = 120.0
alpha = ((xrel - X) / xrel).clamp(0, 1) ** 2 # sponge/wavemaker ramp
def wavemaker(h, hu, hv, t):
ky_s = k_sw * math.sin(theta)
ky_w = k_ww * math.sin(theta)
eta = (a_sw * torch.sin(om_sw * t - ky_s * Y)
+ a_ww * torch.sin(om_ww * t - ky_w * Y + 1.7))
ht = (h0 + eta).clamp(min=0)
u_in = eta * math.sqrt(G / h0)
return ((1 - alpha) * h + alpha * ht,
(1 - alpha) * hu + alpha * ht * u_in,
(1 - alpha) * hv)
def foam_step(foam, h, hu, hv):
safe = h.clamp(min=0.05)
u, v = hu / safe, hv / safe
fr = torch.sqrt(u * u + v * v) / torch.sqrt(G * safe)
wet = (h > 0.03).float()
brk = ((fr - 0.75) / 0.4).clamp(0, 1) * wet
return (foam * math.exp(-dt / 2.5) + dt * 6.0 * brk).clamp(0, 5)
t = 0.0
for _ in range(int(60.0 / dt)): # spin up a steady surf zone
h, hu, hv = wavemaker(h, hu, hv, t)
h, hu, hv = surf.nsw_step(h, hu, hv, b, dx, dy, dt, cf=cf)
foam = foam_step(foam, h, hu, hv)
t += dt
sub = max(1, round(0.2 / dt))
etaF, foamF = [], []
for _ in range(a.frames):
etaF.append(torch.where(h > HDRY, h + b, b).float().cpu().numpy())
foamF.append(foam.float().cpu().numpy())
for _ in range(sub):
h, hu, hv = wavemaker(h, hu, hv, t)
h, hu, hv = surf.nsw_step(h, hu, hv, b, dx, dy, dt, cf=cf)
foam = foam_step(foam, h, hu, hv)
t += dt
et, fo = np.stack(etaF), np.stack(foamF)
bn = b.float().cpu().numpy()
np.save(f"{a.out}_eta.npy", et)
np.save(f"{a.out}_foam.npy", fo)
np.save(f"{a.out}_bed.npy", bn)
print(f"finite={np.isfinite(et).all()} eta[{et.min():.2f}, {et.max():.2f}] m")
print(" cross-shore transect through the peak (y = 0.72 Ly):")
j = int(0.72 * Ny)
crest = et.max(axis=0)
for xm in (150, 300, 380, 420, 460, 500, 540):
i = min(int(xm / Lx * Nx), Nx - 1)
d = -bn[j, i]
print(f" x={xm:3d} m depth={d:6.1f} m crest={crest[j, i]:5.2f} m "
f"foam={fo[:, j, i].max():4.2f}")
if __name__ == "__main__":
main()