Predicting barotropic tides with FES and pyTMD#

Predictions are verified agains sea level observations from the Bayonne-Boucau tide gauge (DATA.SHOM.FR)

pyTMD tutorials

Link to FES data

import os

import numpy as np
import xarray as xr
import pandas as pd

from matplotlib import pyplot as plt

%matplotlib inline

import pynsitu as pin

crs = pin.maps.crs
import pynsitu.tides as td
/Users/aponte/.miniconda3/envs/insitu/lib/python3.10/site-packages/utide/harmonics.py:16: RuntimeWarning: invalid value encountered in cast
  nshallow = np.ma.masked_invalid(const.nshallow).astype(int)
/Users/aponte/.miniconda3/envs/insitu/lib/python3.10/site-packages/utide/harmonics.py:17: RuntimeWarning: invalid value encountered in cast
  ishallow = np.ma.masked_invalid(const.ishallow).astype(int) - 1

load and inspect tide gauge data#

prod = False
tg = xr.open_dataset("data/94_2024.nc")
tg = tg.rename(Source4="sea level", TIME="time")

# extract lon/lat
lon_tg, lat_tg = float(tg.LONGITUDE), float(tg.LATITUDE)

# area of interest
extent = [-3, -1, 43.2, 44.5]
plot_map = lambda: pin.maps.plot_map(extent=extent, land=True, coastline=False)

# time = pd.date_range(start="2023/03/01", end="2023/08/01", freq="30T")
time = pd.to_datetime(tg.time)
tg["sea level"].plot()
[<matplotlib.lines.Line2D at 0x13d548f40>]
_images/8ebba4ff45744b0cfdfccfcb0c1d538a1a2a0bfea7db70ee723d455c56e4d8d6.png

harmonic amplitudes in the area of interest#

# positions of interest
moorings = dict(
    bayonne=[lon_tg - 0.05, lat_tg],  # offset tide gauge to be in water
    offshore=[-2.4, 44.4],
)
mo = (
    pd.DataFrame(moorings)
    .T.rename(columns={0: "lon", 1: "lat"})
    .to_xarray()
    .rename(index="mooring")
)
# output 2d grid
dl_min = 0.0625  # potential resolution: 0.0625
lon, lat = (extent[0], extent[1], dl_min), (extent[2], extent[3], dl_min)  # cp area
# load sea level and semi-diurnal constituents only
vtype = "z"
csts = td.major_semidiurnal
ds_2d = td.load_tidal_amplitudes(lon, lat, vtype, constituents=csts)
ds_moorings = td.load_tidal_amplitudes(mo.lon, mo.lat, vtype, constituents=csts)
broadcasting lon/lat
vmin = 1.32
vmax = 1.34

c = "m2"

# fig, ax = plot_map()
fig, ax, _ = plot_map()

_ds = ds_2d.sel(constituent=c)
(
    np.abs(_ds[vtype + "_amplitude"])
    .rename("abs(amplitude)")
    .plot(
        x="lon",
        y="lat",
        vmin=vmin,
        vmax=vmax,
        ax=ax,
        transform=crs,
    )
)

_ds = ds_moorings.sel(constituent=c)
h = ax.scatter(
    _ds.lon,
    _ds.lat,
    c=np.abs(_ds[vtype + "_amplitude"]),
    s=200,
    edgecolors="orange",
    marker="*",
    # _ds.lon, _ds.lat, c="w", s=100, edgecolors="0.5",
    vmin=vmin,
    vmax=vmax,
    transform=crs,
    zorder=10,
)
_images/02fa596b0f6449c49b03ec1f91ea1ae7c11b2210f3cef541c0414e729f1931be.png
# load on FES grid - needs update
# ds = td.load_raw_tidal_amplitudes(["z", "u", "v"], lon=lon, lat=lat, constituents=td.major_semidiurnal)
# ds.sea_level_phase.sel(constituents="m2").plot()
# np.real(ds.sea_level_amplitude*np.exp(1j*ds.sea_level_phase*np.pi/180)).sel(constituents="m2").plot()

store all tidal harmonics#

# prod = True
prod = False

nc = "bayonne_mooring_harmonics.nc"
if prod:
    ha = td.load_raw_tidal_amplitudes(["z", "u", "v"], lon=lon, lat=lat)
    ha.to_netcdf(nc, mode="w")
else:
    ha = xr.open_dataset(nc)

tidal predictions#

just sea level and (m2, s2)#

# just m2 and s2
ds = td.tidal_prediction(
    mo.lon,
    mo.lat,
    time,
    [
        "z",
    ],
    constituents=td.major_semidiurnal,
    minor=False,
    split=True,
)
ds
<xarray.Dataset> Size: 206kB
Dimensions:       (mooring: 2, time: 3672, constituent: 2)
Coordinates:
  * mooring       (mooring) object 16B 'bayonne' 'offshore'
  * constituent   (constituent) object 16B 'm2' 's2'
    amplitude     (constituent) float64 16B 0.2441 0.1127
    phase         (constituent) float64 16B 1.732 0.0
    omega         (constituent) float64 16B 0.0001405 0.0001454
    alpha         (constituent) float64 16B 0.693 0.693
    species       (constituent) float64 16B 2.0 2.0
    omega_cpd     (constituent) float64 16B 1.932 2.0
  * time          (time) datetime64[ns] 29kB 2024-01-01 ... 2024-06-01T23:00:00
Data variables:
    lon           (mooring) float64 16B -1.565 -2.4
    lat           (mooring) float64 16B 43.53 44.4
    z_tide        (mooring, time) float64 59kB -1.423 -1.311 ... 0.6056 0.8539
    z_tide_split  (mooring, constituent, time) float64 118kB -1.16 ... -0.419

individual constituent contributions#

tlim = ("2024/01/01", "2024/01/15")
da = ds["z_tide_split"].sel(mooring="bayonne", time=slice(*tlim))
da.plot.line(hue="constituent")
[<matplotlib.lines.Line2D at 0x15af81ae0>,
 <matplotlib.lines.Line2D at 0x15af364d0>]
_images/2bed547e6463f86ea1ba63511ddcebfb2ed319ad7f2c39686ee9adebc21f5422.png

total sea level prediction#

# prod = True
prod = False

# with full list of harmonics
nc = "bayonne_moorings_time_series.nc"
if prod:
    tp = td.tidal_prediction(mo.lon, mo.lat, time, split=True)
    tp.to_netcdf(nc, mode="w")
else:
    tp = xr.open_dataset(nc)
def plot_tseries(tg, tp, tlim=None):

    fig, ax = plt.subplots(1, 1, figsize=(10, 5))

    da = tg["sea level"]
    if tlim is not None:
        da = da.sel(time=slice(*tlim))
    da = da - da.mean("time")
    da.plot(color="k", lw=2)
    da_tg = da

    da = tp["z_tide"].sel(mooring="bayonne")
    if tlim is not None:
        da = da.sel(time=slice(*tlim))
    da.plot(hue="mooring")

    # ds["z_tide_minor"].sel(mooring="bayonne").plot()

    residual = da_tg - da
    residual.plot(label="residual")
    residual_rms = residual.std()

    ax.set_title(f"residual rms = {residual_rms*1e2:.1f} cm")

    ax.grid()
plot_tseries(tg, tp)
plot_tseries(tg, tp, tlim=("2024/01/01", "2024/01/15"))
_images/cf5785e1780df88721ba9754f87fb2b44cbc2ea60236d14bb2f174b3d5d7aad0.png _images/27db2afb786265e24c5c5160d021e23275ef38777e0b0d5885fcbafec959ed8c.png

misc: constituents, frequencies, equilibrium tides#

td.cproperties
amplitude phase omega alpha species omega_cpd
constituent
m3 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
eps2 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
n4 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
mtm 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
msqm 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
lambda2 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
mks2 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
r2 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
s4 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
s1 0.000000 0.000000 0.000000e+00 0.000 0.0 0.000000
sa 0.003104 6.232787 1.990970e-07 0.693 0.0 0.002738
ssa 0.019567 3.487600 3.982000e-07 0.693 0.0 0.005476
mm 0.022191 1.964022 2.639200e-06 0.693 0.0 0.036292
msf 0.003681 4.551628 4.925200e-06 0.693 0.0 0.067726
mf 0.042041 1.756042 5.323400e-06 0.693 0.0 0.073202
q1 0.019273 5.877718 6.495854e-05 0.695 1.0 0.893244
o1 0.100661 1.558554 6.759774e-05 0.695 1.0 0.929536
p1 0.046848 6.110182 7.252295e-05 0.706 1.0 0.997262
k1 0.141565 0.173004 7.292117e-05 0.736 1.0 1.002738
j1 0.007915 2.137025 7.556036e-05 0.695 1.0 1.039030
2n2 0.006141 4.086700 1.352405e-04 0.693 2.0 1.859690
mu2 0.007408 3.463115 1.355937e-04 0.693 2.0 1.864547
n2 0.046397 6.050721 1.378797e-04 0.693 2.0 1.895982
nu2 0.008811 5.427137 1.382329e-04 0.693 2.0 1.900839
m2 0.244100 1.731558 1.405189e-04 0.693 2.0 1.932274
l2 0.006931 0.553987 1.431581e-04 0.693 2.0 1.968565
t2 0.006608 0.052842 1.452450e-04 0.693 2.0 1.997262
s2 0.112743 0.000000 1.454441e-04 0.693 2.0 2.000000
k2 0.030684 3.487600 1.458423e-04 0.693 2.0 2.005476
mn4 0.000000 1.499093 2.783984e-04 0.693 0.0 3.828253
m4 0.000000 3.463115 2.810377e-04 0.693 0.0 3.864546
ms4 0.000000 1.731558 2.859630e-04 0.693 0.0 3.932274
m6 0.000000 5.194673 4.215566e-04 0.693 0.0 5.796819
m8 0.000000 6.926230 5.620755e-04 0.693 0.0 7.729093
ax = td.cproperties.amplitude.plot.bar()
ax.set_title("equilibrium tide amplitudes")
ax.set_ylabel("[m]")
ax.grid()
_images/e8324ea11c6d70a01cb2428658bb0b301468fedccc220a586559bc0dab290659.png
fig, axes = plt.subplots(3, 1, figsize=(10, 10))
td.plot_equilibrium_amplitudes(td.cproperties, axes[0])
td.plot_equilibrium_amplitudes(td.cproperties, ax=axes[1], xlim=(0.88, 1.05))
td.plot_equilibrium_amplitudes(td.cproperties, ax=axes[2], xlim=(1.85, 2.01))

for i in range(0, 2):
    axes[i].set_xlabel("")
for i in range(1, 3):
    axes[i].set_title("")
_images/ebd86b82557f809c0b051a54c2ddb66d9eb6a873ed808e0d16a7d6242e669194.png

play with tidal arguments used for predictions#

$ \begin{align} \eta = \mathcal{R} \Big { \sum_k h_k \times f_k(t) h_c e^{i (g_k(t) + u_k(t) ) } \Big } \end{align} $

$g_k (t)$ is the equilibrium argument, it is common to all models $f_l(t)$ and $u_k(t)$ are nodal corrections.

Constituents have unit complex amplitudes here:

ds = td.get_tidal_arguments(time)

_ds = ds
# _ds = ds.isel(time=slice(0,24*28))
np.real(_ds["ht_no_hc"]).sel(constituent=td.major_semidiurnal).sum("constituent").plot()
np.imag(_ds["ht_no_hc"]).sel(constituent=td.major_semidiurnal).sum("constituent").plot()
[<matplotlib.lines.Line2D at 0x14375ea70>]
_images/b37563c5916aa8b72af187e179bf0519305eb3c11fb67721c77d4e38f1ab50b6.png